This document describes how the OpenAPI specification (docs/openapi.yaml) is generated, how to regenerate it after making changes, and how to verify the result.
Audience: Contributors adding or modifying API endpoints.
# Regenerate the spec from current Zod schemas
npm run generate:openapi
# Verify the spec matches registered routes
npx tsx scripts/openapi-drift.jsThe OpenAPI spec is not hand-written. It is generated programmatically from the Zod schemas in src/schemas/ using @asteasolutions/zod-to-openapi.
The generation script is scripts/generate-openapi.ts. It:
- Imports all schemas from
src/schemas/index.ts. - Registers each schema as an OpenAPI component via
registry.registerComponent(). - Registers each route with its request/response schemas via
registry.registerPath(). - Generates the OpenAPI 3.0.0 document.
- Writes the result to
docs/openapi.yaml.
| File | Purpose |
|---|---|
scripts/generate-openapi.ts |
Generation script — all route registrations live here |
src/schemas/index.ts |
Barrel export for all Zod schemas |
src/schemas/*.ts |
Individual schema modules (trust, bond, attestations, etc.) |
docs/openapi.yaml |
The generated output (checked in, do not edit by hand) |
scripts/openapi-drift.js |
Drift detector that compares registered routes against the spec |
After any change to Zod schemas or route registrations, run:
npm run generate:openapiThis overwrites docs/openapi.yaml with a fresh copy. The command is deterministic — running it twice with the same input produces identical output.
Regenerate the spec whenever you:
- Add a new endpoint — register it in
scripts/generate-openapi.tsand add its Zod schemas insrc/schemas/. - Modify a request or response shape — update the Zod schema, then regenerate.
- Remove an endpoint — remove the registration from
scripts/generate-openapi.tsand regenerate.
Create or extend a schema file in src/schemas/. For example, to add a new GET /api/widgets/:id endpoint:
// src/schemas/widgets.ts
import { z } from 'zod';
export const widgetParamsSchema = z.object({
id: z.string().uuid(),
});
export const widgetResponseSchema = z.object({
id: z.string().uuid(),
name: z.string(),
createdAt: z.string().datetime(),
});Export the new schemas from src/schemas/index.ts.
Open scripts/generate-openapi.ts and add a registry.registerPath() call:
import * as schemas from '../src/schemas/index.js';
registry.registerPath({
method: 'get',
path: '/api/widgets/{id}',
summary: 'Get a widget',
description: 'Returns a single widget by ID.',
tags: ['Widgets'],
request: { params: schemas.widgetParamsSchema },
responses: {
200: {
description: 'Widget found',
content: { 'application/json': { schema: schemas.widgetResponseSchema } },
},
404: {
description: 'Widget not found',
content: { 'application/json': { schema: z.any() } },
},
},
});Add the route to the routes array in scripts/openapi-drift.js:
{ path: '/api/widgets/:id', method: 'get' },Note the drift detector uses Express-style :param syntax, while the OpenAPI generator uses {param} syntax.
npm run generate:openapi
npx tsx scripts/openapi-drift.jsBoth commands must pass before pushing.
scripts/openapi-drift.js compares the routes registered in its getRegisteredRoutes() function against the paths present in docs/openapi.yaml.
The script exits non-zero if:
- A route in the code is missing from the spec (you forgot to regenerate).
- A route in the spec does not exist in the code (stale spec or orphaned registration).
- The spec file is empty or malformed.
Admin routes under /api/admin/* are excluded from the public contract check.
npx tsx scripts/openapi-drift.jsExpected output on success:
✅ No OpenAPI contract drift detected. All routes match spec.
# Preview with Redocly
npx @redocly/cli preview-docs docs/openapi.yaml
# Or paste into https://editor.swagger.ioBefore pushing any PR that touches API routes or schemas:
- Run
npm run generate:openapito regeneratedocs/openapi.yaml. - Run
npx tsx scripts/openapi-drift.jsto confirm no drift. - Run
npx tsc --noEmitto verify TypeScript compiles. - Run
npm testto verify tests pass. - Run
npm run lintto verify linting passes. - Run
npm run security:scanandnpm run sbom:checkif the PR adds new dependencies.
- API reference — full endpoint documentation with cURL examples
- Schema conventions — how Zod schemas are structured in this project
- Contributing & Testing — full test suite walkthrough