@codewithagents/openapi-msw
v0.2.1
Published
Generate MSW v2 handlers with seeded Faker mock data from OpenAPI 3.1 specs
Downloads
3,488
Maintainers
Readme
@codewithagents/openapi-msw
Generate MSW v2 HTTP handlers, populated with seeded Faker mock data, from your OpenAPI 3.x spec. Point it at a spec, get a handlers.ts you can drop into a browser worker or a Node test server.
- One handler per operation: every
get,post,put,patch, anddeleteoperation becomes anhttp.<method>(path, resolver)entry in an exportedhandlersarray. - Seeded, deterministic mocks:
faker.seed(<seed>)(default42) is written into the generated file, so every run with the same spec and seed produces identical mock values. Change the seed, re-generate, get a new but still stable dataset. - MSW v2 path syntax: OpenAPI
{id}path params are rewritten to MSW colon style:id. Every param in a path is converted (/orgs/{orgId}/repos/{repoId}becomes/orgs/:orgId/repos/:repoId). - Schema-aware bodies: each handler returns a Faker-generated body shaped from the operation's 2xx JSON response schema, honouring string formats, enums, arrays, and nested objects.
- Prettier-clean output: the generated
handlers.tspassesprettier --checkout of the box. - OpenAPI 3.x: 3.1.x is the primary target; 3.0.x is supported in practice (8 of the 13 showcase specs are 3.0.x and all compile under
tsc --strict).
Install
pnpm add -D @codewithagents/openapi-msw
# or
npm install -D @codewithagents/openapi-mswThe generated handlers.ts imports from msw and @faker-js/faker directly, so both are peer dependencies you install yourself:
pnpm add msw @faker-js/faker| Peer dependency | Range |
|---|---|
| msw | ^2.0.0 |
| @faker-js/faker | ^9.0.0 |
This package depends on openapi-zod-ts for spec parsing and shared config plumbing, but it does not require you to generate models or a client. Run it standalone or alongside the rest of the suite.
Quick start
1. Create openapi-msw.config.json in your project root:
{
"input_openapi": "./spec/api.json",
"output": "./src/mocks",
"seed": 42,
"max_array_items": 3,
"depth_cap": 30
}Only input_openapi and output are required. The three numeric fields are optional and default to the values shown above.
2. Run the generator:
npx openapi-msw
# or point at a config elsewhere:
npx openapi-msw --config ./config/openapi-msw.config.jsonThe CLI takes no positional arguments. Available flags are --config <path>, --help / -h, and --version / -v. When --config is given, relative paths in the config resolve from the config file's directory.
3. handlers.ts appears in your output directory:
The output directory is created recursively if it does not exist. Wire the handlers into MSW as usual:
// browser worker
import { setupWorker } from 'msw/browser'
import { handlers } from './src/mocks/handlers'
export const worker = setupWorker(...handlers)// Node / test server
import { setupServer } from 'msw/node'
import { handlers } from './src/mocks/handlers'
export const server = setupServer(...handlers)Generated output
Given a task API with GET /api/v1/tasks (returns a paged list) and DELETE /api/v1/tasks/{id} (204 No Content), the generated handlers.ts looks like this:
// This file is auto-generated by @codewithagents/openapi-msw, do not edit
import { 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 })),
]A committed, drift-checked sample lives at packages/integration/generated/handlers.ts, generated from packages/integration/spec/api.json.
Config reference
openapi-msw.config.json:
| Field | Required | Default | Description |
|---|---|---|---|
| input_openapi | Yes | n/a | Path to the OpenAPI 3.x spec (JSON or YAML) |
| output | Yes | n/a | Directory to write handlers.ts (created recursively) |
| seed | No | 42 | Passed to faker.seed() in the generated file. Must be an integer >= 0 |
| max_array_items | No | 3 | Length of arrays in generated mocks. Must be an integer >= 1 |
| depth_cap | No | 30 | Schema recursion depth before bailing out to null. Must be an integer >= 1 |
The three optional keys are validated only when present.
Multi-spec generation. Replace the top-level fields with a projects array to generate one handlers.ts per spec. Mixing a projects array with top-level input_openapi / output throws.
{
"projects": [
{ "input_openapi": "./specs/public.json", "output": "./src/mocks/public" },
{ "input_openapi": "./specs/admin.json", "output": "./src/mocks/admin" }
]
}How bodies are generated
The resolver body for each handler is built from the operation's selected 2xx JSON response schema. Faker calls are chosen per schema node:
| 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: max_array_items }, () => item) |
| object with additionalProperties | Object.fromEntries(...) with two generated entries |
| null / unresolvable | null |
Non-obvious behavior
- The seed lives in the output, not the runtime.
faker.seed(<seed>)is the first executable line of the generated file. To change mock values, re-generate with a differentseed. Two runs of the same spec and seed produce byte-identical handlers. - 2xx selection order. The generator prefers a
200response, then201, then the first 2xx code that declares anapplication/jsonbody. A200emitsHttpResponse.json(body)with no status argument; any other 2xx (for example201) emitsHttpResponse.json(body, { status: N }). - No JSON body. If the chosen 2xx response has no
application/jsonschema, the handler emitsHttpResponse.json(null, { status: N })(status from the first 2xx code, defaulting to204). If that response instead declares a non-JSON content type (text/plain,text/csv,application/octet-stream, image, and so on) the handler emitsnew HttpResponse(null, { status: N })rather than falsely claimingapplication/json. Onlyapplication/jsoncontent is mocked with a faker body. allOfis merged;anyOf/oneOfis not unioned. Properties of allallOfmembers are merged into one object and theirrequiredarrays concatenated. ForanyOf/oneOfthe generator picks the first concrete (non-$ref) member, or resolves the first member if all are refs.depth_capbounds recursion. The depth counter increments only on$refresolution and onallOf/anyOf/oneOfresolution steps. Array nesting and plain property access do not count. When depth exceedsdepth_cap, the node emitsnull /* depth cap reached */.- Circular refs terminate cleanly. A
$refback to an already-visited schema name emitsnull /* circular ref: depth cap reached */, distinct from the depth-cap comment, so self-referential schemas do not loop. - Type unions and missing types. For an OpenAPI 3.1
typearray, the generator uses the first element. A schema with notypebut withpropertiesis treated as an object; otherwise it emitsnull. - Path-rooted guard rails.
input_openapiandoutputpaths that resolve into system directories (/etc,/usr,/bin,C:\Windows, and similar) are rejected as likely misconfiguration. This is a guard rail, not a security sandbox.
Programmatic API
The package also exports a programmatic surface for building tooling around it:
import { generateHandlers, generate, loadConfig, loadConfigs } from '@codewithagents/openapi-msw'
// Build the handlers file in memory from a parsed spec.
const { filename, content } = generateHandlers(spec, {
seed: 42,
maxArrayItems: 3,
depthCap: 30,
})
// filename === 'handlers.ts'generate runs the full config-driven flow (load config, parse spec, write files). loadConfig / loadConfigs read and validate a config file. The exported types are MswConfig, HandlerGenOptions, and GeneratedFile.
Part of the openapi-zod-ts suite
openapi-msw is one of five published packages in the codewithagents/openapi-zod-ts monorepo. Four generators emit files from a single OpenAPI spec, and one runtime helper maps API errors in app code:
| Package | Role |
|---|---|
| openapi-zod-ts | Generator: TypeScript models, a native fetch client, and Zod schemas |
| @codewithagents/openapi-server | Generator: framework-agnostic service interface plus an optional hono \| express \| fastify \| none router (default none) |
| @codewithagents/openapi-react-query | Generator: typed React Query v5 hooks over the generated client |
| @codewithagents/openapi-msw | Generator: this package, MSW v2 handlers with seeded Faker mock data |
| @codewithagents/api-errors | Runtime helper: map backend error responses to form-field errors |
See the full-stack reference app (Fastify + React + react-query, with a browser e2e suite) for these packages working together end to end.
License
MIT
