OpenAPI
ZinTrust can generate an OpenAPI 3.0.3 document at runtime from the in-memory route registry. This keeps your API documentation close to your actual routes (and avoids a separate, manually maintained spec file).
This page covers:
- What endpoints are exposed
- How route registrations become an OpenAPI document
- What metadata fields affect the spec
- How to keep the spec stable and low-noise over time
Endpoints
The default documentation routes are registered in routes/openapi.ts:
GET /openapi.json– machine-readable OpenAPI JSONGET /docs– Swagger UI HTML page that renders/openapi.json
The generated spec uses:
Env.APP_NAMEfor the titleEnv.get('APP_VERSION', '0.0.0')for the versionEnv.BASE_URLorhttp://{HOST}:{PORT}for the server URL (when available)
Configuration (Environment)
BASE_URL
If BASE_URL is set (non-empty), it becomes the OpenAPI servers[0].url.
Example:
BASE_URL=https://api.example.comHOST + PORT
If BASE_URL is empty, ZinTrust will try to construct http://{HOST}:{PORT}.
Example:
HOST=localhost
PORT=3000APP_NAME + APP_VERSION
These feed info.title and info.version.
APP_NAME=ZinTrust
APP_VERSION=1.2.3How generation works (high level)
- Routes are registered through
Router.get/post/put/patch/del/.... - The router calls
normalizeRouteMeta(options?.meta)and pushes aRouteRegistrationinto the in-memoryRouteRegistry. OpenApiGenerator.generate(RouteRegistry.list(), options)converts the registry into an OpenAPI document./docsrenders Swagger UI pointing at/openapi.json.
Preventing spec drift (recommended)
Treat the generated OpenAPI as a contract.
This repo includes a snapshot test that generates the OpenAPI document from the registered routes and asserts it matches a committed snapshot:
tests/unit/openapi/OpenApiSpec.snapshot.test.ts
When you intentionally change routes/metadata and expect the OpenAPI to change, update the snapshot as part of the same change:
npm test -- -uRoute Registry → OpenAPI
ZinTrust’s RouteRegistry stores minimal information needed for docs:
export type RouteRegistration = {
method: string;
path: string;
middleware?: readonly string[];
meta?: RouteMeta;
};When generating the spec:
- Paths like
/users/:idare normalized to/users/{id} - Every operation gets a deterministic
operationIdbased on method+path - Request schemas (body/query/headers/params) become OpenAPI
parameters/requestBody - Response schema/status become OpenAPI
responses
OpenAPI generator options
The generator accepts:
export type OpenApiGeneratorOptions = {
title: string;
version: string;
description?: string;
serverUrl?: string;
excludePaths?: readonly string[];
};In the default OpenAPI route, ZinTrust excludes the docs endpoints themselves:
excludePaths: ['/openapi.json', '/docs'];Metadata that affects the spec
OpenAPI output is driven by route metadata. The normalized shape is:
export type RouteMeta = {
summary?: string;
description?: string;
tags?: readonly string[];
request?: {
bodySchema?: ValidationSchema;
querySchema?: ValidationSchema;
paramsSchema?: ValidationSchema;
headersSchema?: ValidationSchema;
};
response?: {
status?: number;
schema?: unknown;
};
};Summary, description, tags
meta.summary→ OpenAPIsummarymeta.description→ OpenAPIdescriptionmeta.tags→ OpenAPItagsarray
Tips:
- Use stable tags (e.g.
['Users'],['Billing'],['Admin']) so Swagger UI groups consistently. - Prefer short summaries; put details in the description.
Path parameters (:id)
If your route path includes :id, the generator will:
- Emit OpenAPI path
/users/{id} - Create a required
pathparameter forid
To type/validate path params, provide meta.request.paramsSchema.
import { Schema } from '@zintrust/core';
Router.get(router, '/users/:id', handler, {
meta: {
summary: 'Get a user',
tags: ['Users'],
request: {
paramsSchema: Schema.create().required('id').integer('id').positiveNumber('id'),
},
response: {
status: 200,
schema: {
type: 'object',
properties: {
id: { type: 'integer' },
email: { type: 'string', format: 'email' },
},
required: ['id', 'email'],
},
},
},
});If you omit paramsSchema, the parameter still exists in the spec, but defaults to { type: 'string' }.
Query / header parameters
meta.request.querySchema and meta.request.headersSchema become OpenAPI parameters.
Router.get(router, '/users', handler, {
meta: {
summary: 'List users',
tags: ['Users'],
request: {
querySchema: Schema.create().integer('page').integer('limit').max('limit', 100),
headersSchema: Schema.create().string('x-client-version'),
},
},
});JSON request body
If meta.request.bodySchema exists, the generator emits requestBody with application/json.
Router.post(router, '/users', handler, {
meta: {
summary: 'Create user',
tags: ['Users'],
request: {
bodySchema: Schema.create()
.required('email')
.email('email')
.required('password')
.minLength('password', 8),
},
response: {
status: 201,
schema: {
type: 'object',
properties: { id: { type: 'integer' } },
required: ['id'],
},
},
},
});Responses
If you don’t specify meta.response, the generator emits a default 200: { description: 'OK' }.
If you specify meta.response.status:
204→ response description becomesNo Content- otherwise → response description is
OK
If you specify meta.response.schema, it becomes responses[status].content['application/json'].schema.
Operation IDs (stability)
Every operation gets an operationId derived from method+path (after converting :id to {id}), for example:
GET /users/{id}→get_users__id_
These IDs are stable as long as your method/path are stable.
If you are using client generation tools, treat method/path changes as breaking.
Security and deployment notes
/openapi.jsonand/docsare public by default. If your API is private, protect these endpoints with middleware or only enable them in non-production environments.- The spec should not contain secrets. Don’t embed tokens in descriptions.
- If you generate clients, ensure the
serverUrlmatches the environment where the client will run.
Troubleshooting
“My route is missing from the spec”
Checklist:
- Ensure the route is registered via
Router.*(so it records intoRouteRegistry). - Ensure the path is not in
excludePaths. - Ensure the code path that registers routes runs before
/openapi.jsonis requested.
“Path params show as string”
Provide meta.request.paramsSchema. Without it, OpenAPI falls back to string for path parameters.
“No request body appears”
Provide meta.request.bodySchema. Only JSON request bodies are emitted today.