@ngandu-dev/zod-openapi
v2.0.1
Published
Decorator-first OpenAPI generation for TypeScript classes and functions using Zod v4 schemas.
Readme
Zod OpenAPI
Decorator-first OpenAPI generation for TypeScript classes and functions using Zod v4 schemas.
This package is designed for legacy or incremental migrations where documentation should stay as metadata on controller methods and route functions instead of becoming application middleware. It uses @asteasolutions/zod-to-openapi under the hood and keeps the authoring experience centered on a single openapi(...) API.
Features
- Zod v4+ only
@openapi(...)method decorator andopenapi(...)(handler)function wrapper- Explicit controller/handler registration and opt-in automatic discovery
- Request body shorthand for the common JSON case
- OpenAPI 3.0 and 3.1 document generation
- Re-exports
zwith.openapi(...)already enabled
Requirements
- Node.js 20.17 or newer
- Zod 4
Installation
bun add @ngandu-dev/zod-openapi zodIf your project uses legacy decorators, enable them in tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}Quick Start
Define a route in its application module. The wrapper attaches OpenAPI metadata and returns the original function with its full TypeScript signature intact:
// src/routes/users.ts
import { openapi, z } from "@ngandu-dev/zod-openapi";
const UserParamsSchema = z.object({
user_id: z.string().min(1),
});
const PermissionsSchema = z
.object({
permissions: z.array(z.string()),
})
.openapi("Permissions");
const UpdatePermissionsResponseSchema = z.object({
id: UserParamsSchema.shape.user_id,
});
export const updatePermissions = openapi({
method: "put",
path: "/api/users/:user_id/permissions",
tags: ["Users"],
summary: "Update User Permissions",
description: "Update permissions for a specific user by their ID.",
request: {
params: UserParamsSchema,
body: PermissionsSchema,
},
responses: {
200: {
description: "Updated permissions",
content: {
"application/json": {
schema: UpdatePermissionsResponseSchema,
},
},
},
},
})(async function updatePermissions(input: {
userId: string;
permissions: string[];
}) {
return {
id: input.userId,
};
});Import the route module and generate the document with automatic discovery:
// src/openapi.ts
import "./routes/users";
import { generateOpenApiDocument } from "@ngandu-dev/zod-openapi";
export const openApiDocument = generateOpenApiDocument({
discovery: "auto",
document: {
openapi: "3.0.0",
info: {
title: "Example API",
version: "1.0.0",
},
},
});
console.log(JSON.stringify(openApiDocument, null, 2));openApiDocument.paths contains /api/users/{user_id}/permissions even though the route used the Express-style :user_id path. The registered Permissions schema is emitted under components.schemas and referenced from the request body.
Automatic discovery is runtime registration, not filesystem scanning. A route module must be imported so that its openapi(...) wrapper or decorators execute.
Explicit Handler Registration
Use handlers when you want a document containing a controlled set of function routes. Explicit discovery is the default:
import { generateOpenApiDocument } from "@ngandu-dev/zod-openapi";
import { updatePermissions } from "./routes/users";
const document = generateOpenApiDocument({
handlers: [updatePermissions],
document: {
openapi: "3.0.0",
info: {
title: "Example API",
version: "1.0.0",
},
},
});controllers, handlers, and routes can be combined in the same document. Passing a function without OpenAPI metadata under handlers throws a descriptive error.
Controller Routes
Class controllers remain supported for legacy applications and incremental migrations:
import { generateOpenApiDocument, openapi } from "@ngandu-dev/zod-openapi";
class HealthController {
@openapi({
method: "get",
path: "/health",
tags: ["System"],
summary: "Health check",
responses: {
200: {
description: "OK",
},
},
})
health() {
return { ok: true };
}
}
const document = generateOpenApiDocument({
controllers: [HealthController],
document: {
openapi: "3.0.0",
info: {
title: "Example API",
version: "1.0.0",
},
},
});Controller classes, instances, inherited methods, and static methods are supported. Controller decorators are also included by discovery: "auto" after their module has been imported.
Migration Notes
Migrating to @ngandu-dev/zod-openapi
Replace the dependency and every import with @ngandu-dev/zod-openapi. Version 2 is a clean
package move and does not provide an alias for the old scope.
Migrating route discovery
Existing controller usage remains compatible and the default discovery mode is still "explicit"; applications using controllers: [UsersController] do not need to change.
- Add standalone routes incrementally with
openapi(route)(handler)and list them underhandlers. - Switch to
discovery: "auto"only after importing every module that declares documented routes. controllers,handlers, and automatic discovery can be combined. Identical handler registrations are deduplicated.- Automatic registrations live for the lifetime of the current process. Prefer explicit sources when generating unrelated documents or when tests require strict isolation.
- Duplicate HTTP method/path operations now throw a descriptive error instead of being silently overwritten. Express-style and OpenAPI-style forms such as
/users/:idand/users/{id}are considered the same path.
Generating a Registry First
If you want to register extra components or mix manual routes with decorated ones, build a registry explicitly:
import {
OpenApiGeneratorV3,
createOpenApiRegistry,
} from "@ngandu-dev/zod-openapi";
import "./routes/users";
const registry = createOpenApiRegistry({
discovery: "auto",
routes: [
{
method: "get",
path: "/health",
tags: ["System"],
summary: "Health check",
responses: {
200: {
description: "OK",
},
},
},
],
register(registry) {
registry.registerComponent("securitySchemes", "bearerAuth", {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
});
},
});
const document = new OpenApiGeneratorV3(registry.definitions).generateDocument({
openapi: "3.0.0",
info: {
title: "Example API",
version: "1.0.0",
},
});OpenAPI 3.1
Use generateOpenApi31Document(...) when you want a 3.1 document:
import "./routes/users";
import { generateOpenApi31Document } from "@ngandu-dev/zod-openapi";
const document = generateOpenApi31Document({
discovery: "auto",
document: {
openapi: "3.1.0",
info: {
title: "Example API",
version: "1.0.0",
},
},
});Development
Install dependencies with bun install, then run bun run quality before opening a pull request.
See CONTRIBUTING.md for the complete contribution workflow.
Testing
Run bun run test for the test suite or bun run test:coverage for a coverage report.
Contributing
Contributions are welcome. Please read CONTRIBUTING.md and follow our Code of Conduct.
Security
Please report vulnerabilities privately as described in SECURITY.md.
License
Released under the MIT License.
