Express Zod API can automatically generate comprehensive OpenAPI 3.1 (formerly Swagger) documentation from your endpoint definitions.
Quick Start
Create a script to generate your documentation:
generate-documentation.ts
import { writeFile } from "node:fs/promises";
import { Documentation } from "express-zod-api";
import { config } from "./config";
import { routing } from "./routing";
import manifest from "./package.json";
await writeFile(
"api-documentation.yaml",
new Documentation({
routing,
config,
version: manifest.version,
title: "Example API",
serverUrl: "https://api.example.com",
}).getSpecAsYaml(),
"utf-8"
);
Run this during your build process to keep documentation in sync with your code.
Configuration Options
Required Options
Your API routing configuration.
Your API server configuration.
The title of your API (appears in the documentation).
The version of your API (semantic versioning recommended).
serverUrl
string | string[]
required
The base URL(s) where your API is hosted. Can be a single URL or an array for multiple environments.
Optional Options
composition
'inline' | 'components'
default:"inline"
How to structure schemas in the documentation:
"inline" - Inline all schemas directly in endpoints
"components" - Extract schemas to a separate reusable components section
hasSummaryFromDescription
Automatically use the first line of description as the summary.
Include HEAD method for each GET endpoint (Express feature).
Custom logic for recognizing headers in input sources.
Adding Descriptions and Examples
Add rich documentation directly to your endpoints:
import { defaultEndpointsFactory } from "express-zod-api";
import { z } from "zod";
const getUserEndpoint = defaultEndpointsFactory.build({
shortDescription: "Retrieves a user by ID",
description: `
This endpoint fetches a single user from the database.
Requires authentication via API key.
`,
input: z.object({
id: z.string()
.example("user_123")
.describe("The unique identifier for the user"),
}),
output: z.object({
id: z.string().example("user_123"),
name: z.string().example("John Doe"),
email: z.string().email().example("john@example.com"),
createdAt: z.string().example("2024-01-01T00:00:00Z"),
}).describe("User object with all details"),
handler: async ({ input }) => {
// Implementation
return {
id: input.id,
name: "John Doe",
email: "john@example.com",
createdAt: new Date().toISOString(),
};
},
});
Examples should be set before transformations, as transformations modify the schema.
Tagging Endpoints
Organize your endpoints into logical groups using tags:
1. Define Tag Constraints
// Declare tags once in your codebase
declare module "express-zod-api" {
interface TagOverrides {
users: unknown;
files: unknown;
subscriptions: unknown;
}
}
2. Tag Your Endpoints
const getUserEndpoint = defaultEndpointsFactory.build({
tag: "users", // Single tag
// or
tag: ["users", "admin"], // Multiple tags
// ... endpoint definition
});
3. Add Tag Descriptions to Documentation
new Documentation({
routing,
config,
title: "My API",
version: "1.0.0",
serverUrl: "https://api.example.com",
tags: {
users: "All operations related to user management",
files: {
description: "File upload and download operations",
url: "https://docs.example.com/files",
},
subscriptions: "Real-time event subscriptions",
},
});
Deprecation Markers
Mark schemas and endpoints as deprecated:
import { z } from "zod";
import { Routing, defaultEndpointsFactory } from "express-zod-api";
// Deprecate a specific field
const endpoint = defaultEndpointsFactory.build({
input: z.object({
newField: z.string(),
oldField: z.string().deprecated(), // Field is deprecated
}),
});
// Deprecate an entire endpoint
const legacyEndpoint = defaultEndpointsFactory.build({
deprecated: true, // All routes using this endpoint are deprecated
// ... rest of definition
});
// Deprecate specific routes
const routing: Routing = {
v1: oldEndpoint.deprecated(), // Deprecates /v1 path
v2: newEndpoint, // Not deprecated
};
Multiple Server URLs
Provide multiple server URLs for different environments:
new Documentation({
routing,
config,
title: "My API",
version: "1.0.0",
serverUrl: [
"https://api.example.com",
"https://staging-api.example.com",
"http://localhost:8080",
],
});
Composition Modes
Inline Mode (Default)
Schemas are defined directly in each endpoint:
paths:
/user:
get:
parameters:
- name: id
schema:
type: string
responses:
200:
content:
application/json:
schema:
type: object
properties:
name:
type: string
Components Mode
Schemas are extracted to a reusable components section:
new Documentation({
// ... other options
composition: "components",
});
paths:
/user:
get:
responses:
200:
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
components:
schemas:
UserResponse:
type: object
properties:
name:
type: string
Use "components" mode to reduce file size when schemas are reused across endpoints.
Custom Descriptions
Generate dynamic descriptions for different components:
new Documentation({
routing,
config,
title: "My API",
version: "1.0.0",
serverUrl: "https://api.example.com",
descriptions: {
positiveResponse: ({ method, path, statusCode }) =>
`Successful ${method.toUpperCase()} response for ${path} (${statusCode})`,
negativeResponse: ({ method, path, statusCode }) =>
`Error response for ${method.toUpperCase()} ${path} (${statusCode})`,
requestParameter: ({ operationId }) =>
`Parameters for operation ${operationId}`,
requestBody: ({ method, path }) =>
`Request body for ${method.toUpperCase()} ${path}`,
},
});
Custom Schema Names
Provide custom names for schemas using metadata:
const userSchema = z.object({
id: z.string(),
name: z.string(),
}).meta({ id: "User" });
const endpoint = defaultEndpointsFactory.build({
output: userSchema,
// ...
});
The schema will appear as User in the components section when using composition: "components".
Branded Types
Customize how branded types appear in documentation:
import { z } from "zod";
import { Documentation, Depicter } from "express-zod-api";
const myBrand = Symbol("UserId");
const userIdSchema = z.string().brand(myBrand);
const customHandler: Depicter = (
{ zodSchema, jsonSchema },
{ path, method, isResponse }
) => ({
...jsonSchema,
type: "string",
pattern: "^user_[a-z0-9]+$",
example: "user_123abc",
description: "A unique user identifier",
});
new Documentation({
routing,
config,
title: "My API",
version: "1.0.0",
serverUrl: "https://api.example.com",
brandHandling: {
[myBrand]: customHandler,
},
});
Serving Documentation
Use the generated YAML with Swagger UI:
import express from "express";
import swaggerUi from "swagger-ui-express";
import { createConfig } from "express-zod-api";
import YAML from "yaml";
import { readFileSync } from "fs";
const spec = YAML.parse(
readFileSync("api-documentation.yaml", "utf-8")
);
const config = createConfig({
beforeRouting: ({ app }) => {
app.use("/docs", swaggerUi.serve, swaggerUi.setup(spec));
},
// ... other config
});
Now visit https://your-api.com/docs to view your interactive documentation.
Complete Example
import { writeFile } from "node:fs/promises";
import { Documentation } from "express-zod-api";
import { config } from "./config";
import { routing } from "./routing";
import manifest from "./package.json";
// Define tags
declare module "express-zod-api" {
interface TagOverrides {
users: unknown;
admin: unknown;
public: unknown;
}
}
const documentation = new Documentation({
routing,
config,
version: manifest.version,
title: "My Awesome API",
serverUrl: [
"https://api.example.com",
"https://staging.example.com",
],
composition: "components",
hasSummaryFromDescription: true,
hasHeadMethod: true,
tags: {
users: "User management operations",
admin: "Administrative operations",
public: "Publicly accessible endpoints",
},
});
try {
await writeFile(
"openapi.yaml",
documentation.getSpecAsYaml(),
"utf-8"
);
console.log("✓ Documentation generated successfully");
} catch (error) {
console.error("Failed to generate documentation:", error);
process.exit(1);
}
Next Steps