Skip to content

StudioStudio

Studio

A development server with a web UI: inspect requests, explore endpoints, call them from a playground, and run mock servers.

12 min readboth modes

Overview

Courier Studio is a development server with a web UI: a request inspector, an endpoint explorer, an API playground, mock servers, and more. You run it next to your app, and your app connects to it — the server side through attachRuntime(), the browser side through one of the build integrations below.

Install

In one command

npx @noverax/create-courier sets Studio up with the rest: both packages, the integration for your framework, a mock API to work against, and a studio script to start it with. See Create Courier.

By hand:

bash
npm install --save-dev @noverax/courier-studio
npm install @noverax/courier-studio-core

Studio needs Node.js 22.13 or newer. Studio is a development tool, so it is a dev dependency. @noverax/courier-studio-core is what your server instrumentation imports attachRuntime() from, so it is a dependency: a production install leaves dev dependencies out, and a server that imports the runtime does not start without it — even one that calls it only in development. attachRuntime() runs in production too, where it registers the config's sources. An app that only connects its browser through a build integration can leave it out.

Start Studio

bash
npx @noverax/courier-studio

Studio listens on http://localhost:6280 — at both 127.0.0.1 and ::1, so it answers whichever one a client dials — or on the studio.host and studio.port of your courier.config.ts. Server runtimes and the build plugins read the same block, so moving Studio there moves them with it: a server runtime reads the nearest config at or above its working directory, and a build plugin the nearest at or above Vite's root — or, for webpack, esbuild and Rollup, the working directory. Flags: --port, --host, --cwd (project root), --open (open a browser), --version. Its commands run without the server: courier-studio openapi writes the sources as OpenAPI documents, courier-studio verify checks recorded calls against the definition, courier-studio diff reports what a change to the definition breaks for its callers, and courier-studio mcp serves Studio to an editor's agent. Studio keeps its working files — imported API state, copies of the files it replaced or removed, saved sessions — in .courier/, which ignores itself in git.

What Studio shows is kept in .courier/studio.db, a SQLite database built into Node, with nothing to install or set up. The requests it captured are there, the latest 2000 of them, so a page that reloads, or a Studio that restarts, shows them again. A request still open when Studio stopped is shown as ended, since no runtime can say any more how it ended. The saved sessions are there too, and which one a page loaded: a page that reloads shows that session again, recording paused, until you go back to the live requests with the play button or Back to live requests in the command palette. A session shared as a session file opens in a colleague's Studio the same way. So are the requests each mock answered, the latest 500, which the Mocks view's log shows again, and the Playground's history of calls. So is what you set, and what you leave unfinished: the settings; the requests you pin, which the log keeps as it lets its oldest go; the Playground's drafts; Get started's progress; a mock wizard or an API import left midway, which opens again where it was; and the network conditions and the scenario set on each mock, which it has again when it starts again — unless the mocks file now declares other network conditions, the newer word. All of it is kept for the project, so another browser shows it too; a page opened without the token sees it but keeps its own changes to itself. The browser keeps only the page's token, and a copy of the settings so the page opens in its theme before Studio answers. Studio creates the file readable and writable by its owner only.

Settings → Storage shows the room each part of .courier/ takes — the database on disk and the data in it, the captured requests, the mocks' logs, the Playground's history, the saved sessions, the mocks' stores, and the backups of the files imports replaced — and clears any of them, or all at once, after asking. A mock that runs resets its store to its seeds; the backups an undo still needs stay. The database gives the room its deleted rows took back to the disk as it goes, and is compacted after each clear, so it does not keep growing as the logs let their oldest requests go. Once it grows past the size set there, 500 MB to start with, a line under the header says so. To start empty, clear everything there — or delete the file with Studio stopped.

No credential is written there. The value of Authorization, Proxy-Authorization, Cookie, Set-Cookie, and of any header whose name has a word such as auth, token, secret, password, session, csrf or api-key in it, is kept as [redacted] in the request log, the mocks' logs, saved sessions and the Playground's history, and so in a HAR or Postman export of a session. The live view, which nothing writes down, shows the values whole to a page opened from Studio's link, and redacted to any other; a request replayed after a reload carries [redacted] instead.

A body larger than 16 KB, sent or answered, is kept and shown as a part of it: the first items of a list, the small fields of an object and a part of each large one, the start of a text, each part left out named where it was, as "… 45,800 more items", beside the size of the whole. Download whole in the request's Response or Request tab saves the whole body as a file, from the app or the mock that holds it: each keeps the whole bodies of its latest requests, up to 64 MB, a page in the browser up to 16 MB. A replay sends the whole variables, never the part, and is refused once they are gone; a snippet of a request whose body is too large to write out sends it from the file Download whole saves. A saved session keeps the parts, and its HAR and Postman exports say how large each whole was. A page that connects is sent each log's newest requests, each whole, as many as fit 16 MB.

When the database cannot be kept there — .courier is a link, the disk is read-only, a newer Studio made it — Studio keeps its data in memory for that run, and says so. A file there that is not a database is moved aside to studio.db.broken-<time>, and a new one is started.

Studio loads courier.config.ts from the project root, with the courier.mocks.ts beside it, and loads them again whenever either one, or a module either imports, is saved: see How Studio loads the project.

A project that has no API sources yet starts with this courier.config.ts at its root — the APIs page offers it to copy while there is no config, and keeps Import API off until one loads:

ts
import { defineConfig } from "@noverax/courier/config";
export default defineConfig({ sources: {} });

Import API then writes the project's first API into its output directory — src/api/<source name>/ by default — where source.ts exports the source under its source name. Add it to sources under that name, and the APIs page lists it as imported:

ts
import { defineConfig } from "@noverax/courier/config";
import { petstore } from "./src/api/petstore/source";

export default defineConfig({ sources: { petstore } });

Studio makes changes only for a page opened from the link it prints in its terminal, http://localhost:6280/#token=…. The token is the project's: Studio keeps it in .courier/studio-token, readable by your account alone, and prints the same one each time it starts, so a page you opened once keeps working; the page takes the token out of its address bar and keeps it for its next visits. A page or process without it still sees everything Studio shows, the credentials of live traffic redacted, but Studio refuses its every change: writing the project's files, driving the mocks, sending from the Playground, saving, loading or deleting sessions, and clearing the log. Studio answers only its own page and the apps running on this machine, so on the default localhost nothing else can reach it; bound to the network (--host 0.0.0.0), anyone on that network can see what it shows, and a change still needs the token. Its pages refuse to be shown in a frame, so another site cannot put Studio under a page of its own. The Playground runs a call through your application's server, which has the same token: its runtime reads .courier/studio-token beside courier.config each time it connects to Studio. Studio sends Playground calls, and the headers typed into them, only to a runtime that presented it; any other process that connects as a runtime has its traffic shown in the Timeline and is sent no call, and the Playground says why.

The build integrations connect your app to Studio; they do not start it.

Connect the browser

Every integration connects only a development build, as the bundler tells one: the dev server of Vite, Astro or Nuxt, next dev, webpack's mode: "development", an esbuild build that neither minifies nor defines process.env.NODE_ENV as "production", and Rollup's watch mode (rollup -c -w). A production build — vite build, webpack --mode production or webpack with no mode, a minified esbuild build, a one-off rollup -c — is left untouched, whether or not NODE_ENV is set, and NODE_ENV=production rules out any build. Pass { enabled: true } or { enabled: false } to decide yourself.

Import pathForRegister
@noverax/courier-studio/viteVite, and the frameworks built on itplugins: [courierStudio()]
@noverax/courier-studio/react, /vue, /sveltekit, /remix, /solidReact, Vue, SvelteKit, React Router (Remix), SolidStart — the Vite plugin under a framework nameplugins: [courierStudio()] in vite.config.ts
@noverax/courier-studio/astroAstrointegrations: [courierStudio()]
@noverax/courier-studio/nuxtNuxt, with any of its buildersmodules: [courierStudioModule]
@noverax/courier-studio/nextNext.js under webpackexport default withCourierStudio()(nextConfig)
@noverax/courier-studio/webpackwebpackplugins: [courierStudio()]
@noverax/courier-studio/esbuildesbuildplugins: [courierStudio()]
@noverax/courier-studio/rollupRollupplugins: [courierStudio()]
@noverax/courier-studio/angularAngularsetupCourierStudio() in main.ts
ts
// vite.config.ts
import { courierStudio } from "@noverax/courier-studio/vite"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [courierStudio()],
})

Next.js

Next.js 16 builds with Turbopack, which runs no webpack plugins. Connect the browser from instrumentation-client.ts (Next.js 15.3 and later, under either bundler):

ts
// instrumentation-client.ts
import { attachRuntime } from "@noverax/courier-studio-core/runtime"

if (process.env.NODE_ENV === "development") {
  attachRuntime("next-browser")
}

A browser bundle cannot read courier.config.ts, so when Studio is not at the default address, pass it through env: { COURIER_STUDIO_URL: "http://localhost:6290" } in next.config.ts.

Under webpack — Next.js 14 and 15 by default, next dev --webpack since — withCourierStudio() connects the browser without that file. It also keeps ws, which the server's Studio connection runs on, out of the server bundle: bundled, ws fails on every frame of 48 bytes or more, and the server never reaches Studio. Without the wrapper, add serverExternalPackages: ["ws"] to the config. Under Turbopack it leaves your config untouched and prints the snippet above if the file is missing.

Nuxt

Import the module rather than naming it as a string, and pass options in the tuple form:

ts
// nuxt.config.ts
import { courierStudioModule } from "@noverax/courier-studio/nuxt"

export default defineNuxtConfig({
  modules: [courierStudioModule],
})

Angular

ts
// src/main.ts
import { setupCourierStudio } from "@noverax/courier-studio/angular"

setupCourierStudio() // or setupCourierStudio({ studioUrl: "http://localhost:6290" })

bootstrapApplication(AppComponent, appConfig)

Connect the server

ts
import { attachRuntime } from "@noverax/courier-studio-core/runtime"

await attachRuntime()

Call it once at startup — from instrumentation.ts in Next.js, a Nitro plugin in Nuxt, hooks.server.ts in SvelteKit, or before listen() in a Node server. The runtime is @noverax/courier-studio-core's; its README describes the options attachRuntime() takes.

Studio keeps the latest 2000 requests your app makes in memory, so a Studio page that opens or reloads shows the ones made before it; clearing the Timeline clears them there too, except the pinned ones when Preserve log is on. A request or stream still open when its app stops, or loses its connection to Studio, is shown as failed: the app can no longer say how it ended.

Host a public demo

Studio can show your project to people who do not have it: a demo of your API on a public address. Run it from code with demo:

ts
import { createStudioServer } from "@noverax/courier-studio/server"

createStudioServer({
  host: "0.0.0.0",
  port: 6280,
  demo: { resetsAt: Date.now() + 60 * 60_000 },
  extraCorsOrigins: ["https://demo.example.com"],
})

A visitor — a page opened without the token — sees everything Studio shows, the credentials of live traffic redacted, and may also send from the Playground and drive the mocks: start and stop them, set their latency and failures, apply scenarios, change their data. Limits keep one visitor from crowding out the rest, counted every ten seconds for each page, for each visitor's address — so opening more pages does not widen a share — and for all visitors together:

  • Playground: ten calls from a page, twenty from an address and sixty from all, with four open at once from a page.
  • Mocks: twenty commands and five starts or stops from a page, thirty and ten from an address; a seed of up to 200 records, records added to a collection only while it holds fewer than a thousand, and a latency of up to ten seconds.
  • Studio's API: twenty requests from an address and sixty from all, two at work at once — a specification parsed, its code generated.
  • The page itself: six hundred requests from an address, six thousand from all; ten sockets open for an address and two hundred in all, ten opened every ten seconds for an address and forty in all; a thousand connections at most, each closed when its request's headers take more than ten seconds.
  • Size: nothing larger than 64 KB at a time from a visitor; a socket reads no message larger than 512 KB, and each page is sent the newest 2 MB of each log as it connects.

Past a limit, the visitor is told which one and when to try again: a request is answered with 429 and Retry-After. A visitor's clears stay in their own page, and their Playground history is their own. Studio still writes no file: importing an API, generating mocks and saving a session answer that the demo does not, and nothing every visitor sees is cleared. A visitor's import reads a specification pasted, or fetched from the public internet only — never from the machine Studio runs on or its network. The page opened with the token keeps every right.

Behind a proxy that reports the visitor

Studio counts a visitor by the address the proxy in front of it reports last in X-Forwarded-For, believed only from a proxy on this machine or a private network. Traefik, Caddy and nginx's usual configuration send it. Behind a proxy that does not, every visitor shares the proxy's address and its limits.

The Playground calls the project's own sources only, through the runtime that serves them, so point those sources at servers anyone may call. resetsAt is when you start the demo again from a clean state; each page counts down to it. extraCorsOrigins names the address visitors open when Studio sits behind a proxy on a domain of its own. examples/demo-shop is a complete demo: two backends, a server, traffic, two mocks, a reset every hour, and the container it runs in.

Next steps

  • Import an API — turn an OpenAPI, Swagger or Postman specification into typed API code
  • Mock servers — declare mocks Studio runs, and add them from its UI
  • Network conditions — make a mock slow or failing, from the Network tab or in code, and halt a run on its own
  • How Studio loads the project — reloads, failures, the transform cache, and running Studio from code
  • VS Code extension — Studio's figures above each call in your code, and the call one click from Studio