Types and fetch client
openapi-zod-ts generates the typed models.ts and fetch client your app calls. openapi-msw mocks those same calls in the browser and in tests.
@codewithagents/openapi-msw reads your OpenAPI 3.x spec and writes a single handlers.ts file of Mock Service Worker v2 HTTP handlers, one per operation, each returning a Faker-generated body for the operation’s 2xx JSON response. Point it at the same spec your client and server use, drop the handlers into setupWorker (browser) or setupServer (Node and tests), and you have realistic, type-shaped mock responses without hand-writing a single fixture. Output is deterministic: a faker.seed(...) call is written at the top of the file, so the same spec and seed always produce identical values.
Running the generator produces one file in your output directory:
| File | Contents |
|---|---|
handlers.ts | One http.<method>(path, resolver) handler per operation, wrapped in export const handlers = [] |
Key guarantees:
get, post, put, patch, and delete operation in the spec becomes an http.<method> handler. Operations missing responses are skipped.email, uuid, date-time, uri, enums, and so on).faker.seed(<seed>) call (default 42) is emitted at the top of the file, so re-running with the same spec and seed produces identical mock values. Change the seed and re-generate to get a different fixed set.{id} path parameters are rewritten to MSW’s colon form :id. Every parameter in a path is converted, so /orgs/{orgId}/repos/{repoId} becomes /orgs/:orgId/repos/:repoId.handlers.ts passes prettier --check with your project’s own Prettier config.tsc --strict. Supports $ref, allOf, anyOf, oneOf, and enums, with depth and circular-reference guards.npm i -D @codewithagents/openapi-mswpnpm add -D @codewithagents/openapi-mswyarn add -D @codewithagents/openapi-mswThe generated handlers.ts imports http/HttpResponse from msw and faker from @faker-js/faker. Both are peer dependencies, so install them as regular dependencies in the consumer project:
npm i msw @faker-js/fakerpnpm add msw @faker-js/fakeryarn add msw @faker-js/faker1. Create openapi-msw.config.json in your project root:
{ "input_openapi": "./spec/api.json", "output": "./src/mocks"}2. Run the generator:
npx openapi-mswOr add a script to package.json:
{ "scripts": { "generate:mocks": "openapi-msw" }}This writes ./src/mocks/handlers.ts (the output directory is created recursively if missing).
3. Wire the handlers into MSW:
import { setupWorker } from 'msw/browser'import { handlers } from './handlers'
export const worker = setupWorker(...handlers)import { setupServer } from 'msw/node'import { handlers } from './handlers'
export const server = setupServer(...handlers)The config file must be valid JSON and must have a .json extension. By default the generator looks for openapi-msw.config.json in the current working directory. Use --config <path> to point at a different file.
npx openapi-msw --config ./config/openapi-msw.config.json| Field | Type | Required | Default | Description |
|---|---|---|---|---|
input_openapi | string | Yes | n/a | Path to your OpenAPI spec (JSON or YAML) |
output | string | Yes | n/a | Directory to write handlers.ts |
seed | number | No | 42 | Faker seed written into the generated file. Integer >= 0 |
max_array_items | number | No | 3 | Number of items generated for array schemas. Integer >= 1 |
depth_cap | number | No | 30 | Maximum schema-resolution depth before emitting null. Integer >= 1 |
Only input_openapi and output are required. The three optional keys are validated only when present, and their defaults are applied at generation time if omitted.
Full config example:
{ "input_openapi": "./spec/api.json", "output": "./src/mocks", "seed": 42, "max_array_items": 3, "depth_cap": 30}To generate handlers for more than one spec from a single config, use a top-level projects array. Each entry takes its own input_openapi, output, and optional keys:
{ "projects": [ { "input_openapi": "./spec/orders.json", "output": "./src/mocks/orders" }, { "input_openapi": "./spec/billing.json", "output": "./src/mocks/billing", "seed": 7 } ]}The CLI takes no positional arguments, only flags:
| Flag | Description |
|---|---|
--config <path> | Path to the config file. Defaults to openapi-msw.config.json in the cwd |
--help, -h | Print usage and exit |
--version, -v | Print the installed version and exit |
# Use openapi-msw.config.json in the current directorynpx openapi-msw
# Point at a config elsewherenpx openapi-msw --config ./config/openapi-msw.config.jsonGiven a task API with GET /api/v1/tasks (returns a paged list) and DELETE /api/v1/tasks/{id} (returns 204), the generator writes:
// This file is auto-generated by @codewithagents/openapi-msw, do not editimport { http, HttpResponse } from 'msw'import { faker } from '@faker-js/faker'
faker.seed(42)
export const handlers = [ http.get('/api/v1/tasks', () => HttpResponse.json({ items: Array.from({ length: 3 }, () => ({ id: faker.string.uuid(), title: faker.lorem.word(), status: faker.helpers.arrayElement(['pending', 'in_progress', 'done']), priority: faker.number.int({ min: 1, max: 1000 }), assigneeEmail: faker.internet.email(), createdAt: faker.date.recent().toISOString(), })), total: faker.number.int({ min: 1, max: 1000 }), }) ), http.delete('/api/v1/tasks/:id', () => HttpResponse.json(null, { status: 204 })),]The {id} path parameter is rewritten to MSW’s :id, and each field maps to a Faker call chosen from its schema type and format.
| Schema | Generated value |
|---|---|
string, format email | faker.internet.email() |
string, format uuid | faker.string.uuid() |
string, format date-time | faker.date.recent().toISOString() |
string, format date | faker.date.recent().toISOString().slice(0, 10) |
string, format uri / url | faker.internet.url() |
string (other) | faker.lorem.word() |
integer, int32, int64 | faker.number.int({ min: 1, max: 1000 }) |
number (other) | faker.number.float({ min: 0, max: 1000, fractionDigits: 2 }) |
boolean | faker.datatype.boolean() |
enum | faker.helpers.arrayElement([...literals]) |
array | Array.from({ length: maxArrayItems }, () => item) |
null / unresolvable | null |
The package also exposes a programmatic API for use inside your own scripts:
import { generateHandlers } from '@codewithagents/openapi-msw'
const { filename, content } = generateHandlers(spec, { seed: 42, maxArrayItems: 3, depthCap: 30,})// filename === 'handlers.ts'generate, loadConfig, and loadConfigs are also exported for driving the full config-to-file flow, along with the MswConfig, HandlerGenOptions, and GeneratedFile types.
These rules are not obvious from the config alone, so they are worth keeping in mind:
200, then 201, then the first 2xx code with an application/json body. A 200 emits HttpResponse.json(body) with no status argument; any other 2xx code (for example 201) emits HttpResponse.json(body, { status: N }).application/json schema emits HttpResponse.json(null, { status: N }) (status from the first 2xx code, defaulting to 204). If that response instead declares a non-JSON content type (text/plain, octet-stream, image, and so on) it emits new HttpResponse(null, { status: N }), so it never falsely claims application/json. Only application/json responses get a Faker body.allOf is merged; anyOf/oneOf pick the first. Properties of all allOf members are merged into one object and their required arrays concatenated. For anyOf and oneOf the generator picks the first concrete (non-$ref) member; if all members are refs it resolves the first one.depth_cap bounds recursion. The depth counter increments only on $ref resolution and on allOf/anyOf/oneOf resolution. Array nesting and plain property access do not increment it. When depth exceeds the cap, the generator emits null /* depth cap reached */.$ref back to an already-visited schema name emits null /* circular ref: depth cap reached */, distinct from the depth-cap comment, so self-referential schemas do not loop forever.type be an array; the generator uses only the first element. Schemas with no type fall back to object generation when properties is present, otherwise emit null.operationId is not required. A handler is driven by its path and method, so operations without an operationId still generate.Types and fetch client
openapi-zod-ts generates the typed models.ts and fetch client your app calls. openapi-msw mocks those same calls in the browser and in tests.
React Query hooks
Add @codewithagents/openapi-react-query to generate typed useQuery and useMutation hooks, then mock their requests with the generated handlers during development and tests.
Server interface
Add @codewithagents/openapi-server to implement the same spec on the server with a typed service interface and an optional fastify, hono, or express router.