Skip to main content
Express Zod API generates OpenAPI 3.1 (formerly Swagger) compliant documentation, the industry standard for describing RESTful APIs.

What is OpenAPI 3.1?

OpenAPI 3.1 is a specification for describing HTTP APIs in a machine-readable format. It provides:
  • Standardized API documentation
  • Interactive API explorers (like Swagger UI)
  • Code generation for clients and servers
  • API testing and validation tools
  • JSON Schema compatibility
OpenAPI 3.1 fully embraces JSON Schema, making it more powerful than previous versions.

Key Features

Express Zod API’s OpenAPI generator provides:

Automatic Generation

Documentation is generated directly from your Zod schemas and endpoint definitions

Type Safety

Your TypeScript types and OpenAPI schemas are always in sync

JSON Schema

Full JSON Schema compatibility for maximum interoperability

Rich Metadata

Support for examples, descriptions, deprecation, and more

Basic Usage

Generate OpenAPI documentation:
import { Documentation } from "express-zod-api";
import { routing } from "./routing";
import { config } from "./config";

const openapi = new Documentation({
  routing,
  config,
  version: "1.0.0",
  title: "My API",
  serverUrl: "https://api.example.com",
});

// Get as YAML string
const yaml = openapi.getSpecAsYaml();

// Or access the spec object directly
const spec = openapi.rootDoc;

Schema Mapping

Here’s how Zod schemas map to OpenAPI types:
Zod SchemaOpenAPI TypeNotes
z.string()type: string
z.number()type: number
z.boolean()type: boolean
z.object()type: objectWith properties
z.array()type: arrayWith items
z.enum()enum: [...]Enumeration values
z.literal()const: valueSingle value
z.union()anyOf: [...]Multiple schemas
z.intersection()allOf: [...]Combined schemas
z.optional()No requiredProperty is optional
z.nullable()type: ["...", "null"]OpenAPI 3.1 style
ez.dateIn()type: string, format: date-timeISO 8601 string
ez.dateOut()type: string, format: date-timeISO 8601 string
ez.upload()type: string, format: binaryFile upload

Security Schemes

Express Zod API automatically generates security schemes from your middleware definitions:
import { Middleware } from "express-zod-api";
import { z } from "zod";

const authMiddleware = new Middleware({
  security: {
    // Generates API key security scheme
    and: [
      { type: "input", name: "apiKey" },
      { type: "header", name: "X-API-Token" },
    ],
  },
  input: z.object({
    apiKey: z.string().min(1),
  }),
  handler: async ({ input, request }) => {
    // Validate authentication
    const token = request.headers["x-api-token"];
    // ...
    return { userId: "123" };
  },
});
This generates:
securitySchemes:
  INPUT_1:
    type: apiKey
    in: query
    name: apiKey
  HEADER_1:
    type: apiKey
    in: header
    name: X-API-Token

Security Types

Supported security scheme types:
type: 'input'
object
API key in query parameter or request body
  • name: Parameter name
type: 'header'
object
API key in request header
  • name: Header name (case-insensitive)
API key in cookie
  • name: Cookie name
type: 'http'
object
HTTP authentication (Basic, Bearer, etc.)
  • scheme: Authentication scheme (e.g., “bearer”, “basic”)
type: 'oauth2'
object
OAuth 2.0 authentication
  • flows: OAuth flows configuration
type: 'openIdConnect'
object
OpenID Connect Discovery
  • url: OpenID Connect URL

Request and Response Examples

Add examples to your schemas for better documentation:
import { z } from "zod";
import { defaultEndpointsFactory } from "express-zod-api";

const createUserEndpoint = defaultEndpointsFactory.build({
  method: "post",
  input: z.object({
    name: z.string()
      .min(1)
      .example("John Doe")
      .describe("User's full name"),
    email: z.string()
      .email()
      .example("john@example.com")
      .describe("User's email address"),
    age: z.number()
      .int()
      .positive()
      .optional()
      .example(30)
      .describe("User's age in years"),
  }),
  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"),
  }),
  handler: async ({ input }) => {
    // Implementation
    return {
      id: "user_123",
      name: input.name,
      email: input.email,
      createdAt: new Date().toISOString(),
    };
  },
});
Generates:
requestBody:
  content:
    application/json:
      schema:
        type: object
        properties:
          name:
            type: string
            description: User's full name
            example: John Doe
          email:
            type: string
            format: email
            description: User's email address
            example: john@example.com

Path Parameters

Path parameters are automatically detected and documented:
import { Routing } from "express-zod-api";
import { z } from "zod";

const getUserEndpoint = defaultEndpointsFactory.build({
  input: z.object({
    userId: z.string()
      .describe("The unique user identifier"),
  }),
  // ...
});

const routing: Routing = {
  user: {
    ":userId": getUserEndpoint,
  },
};
Generates:
paths:
  /user/{userId}:
    get:
      parameters:
        - name: userId
          in: path
          required: true
          description: The unique user identifier
          schema:
            type: string

Response Status Codes

Document different status codes using custom result handlers:
import { ResultHandler } from "express-zod-api";
import { z } from "zod";

const customResultHandler = new ResultHandler({
  positive: (data) => ({
    statusCode: [200, 201], // OK or Created
    schema: z.object({ 
      status: z.literal("success"), 
      data 
    }),
  }),
  negative: [
    {
      statusCode: 400, // Bad Request
      schema: z.object({ 
        status: z.literal("error"), 
        message: z.string() 
      }),
    },
    {
      statusCode: 401, // Unauthorized
      schema: z.object({ 
        status: z.literal("unauthorized") 
      }),
    },
    {
      statusCode: 500, // Internal Server Error
      schema: z.object({ 
        status: z.literal("error"), 
        message: z.string() 
      }),
    },
  ],
  handler: ({ error, response, output }) => {
    // Implementation
  },
});

Content Types

Express Zod API supports various content types:

JSON (Default)

// application/json - default for most endpoints
const endpoint = defaultEndpointsFactory.build({
  input: z.object({ name: z.string() }),
  // ...
});

Form Data

import { ez } from "express-zod-api";

// application/x-www-form-urlencoded
const formEndpoint = defaultEndpointsFactory.build({
  method: "post",
  input: ez.form({
    name: z.string(),
    email: z.string().email(),
  }),
  // ...
});

File Uploads

import { ez } from "express-zod-api";

// multipart/form-data
const uploadEndpoint = defaultEndpointsFactory.build({
  method: "post",
  input: z.object({
    avatar: ez.upload(),
  }),
  // ...
});

Raw Data

import { ez } from "express-zod-api";

// application/octet-stream
const rawEndpoint = defaultEndpointsFactory.build({
  method: "post",
  input: ez.raw({}),
  // ...
});

Using Generated Documentation

With Swagger UI

import express from "express";
import swaggerUi from "swagger-ui-express";
import YAML from "yaml";
import { readFileSync } from "fs";
import { createConfig } from "express-zod-api";

const spec = YAML.parse(
  readFileSync("openapi.yaml", "utf-8")
);

const config = createConfig({
  beforeRouting: ({ app }) => {
    app.use(
      "/docs",
      swaggerUi.serve,
      swaggerUi.setup(spec, {
        explorer: true,
        customCss: '.swagger-ui .topbar { display: none }',
      })
    );
  },
  // ... other config
});

With Redoc

import { createConfig } from "express-zod-api";
import { readFileSync } from "fs";

const config = createConfig({
  beforeRouting: ({ app }) => {
    const spec = readFileSync("openapi.yaml", "utf-8");
    
    app.get("/docs", (req, res) => {
      res.send(`
        <!DOCTYPE html>
        <html>
          <head>
            <title>API Documentation</title>
            <meta charset="utf-8"/>
            <meta name="viewport" content="width=device-width, initial-scale=1">
            <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
          </head>
          <body>
            <redoc spec-url="/openapi.yaml"></redoc>
            <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
          </body>
        </html>
      `);
    });
    
    app.get("/openapi.yaml", (req, res) => {
      res.type("text/yaml").send(spec);
    });
  },
});

With API Clients

Generate clients using OpenAPI Generator:
# Generate TypeScript Axios client
npx @openapitools/openapi-generator-cli generate \
  -i openapi.yaml \
  -g typescript-axios \
  -o ./generated-client

# Generate Python client
npx @openapitools/openapi-generator-cli generate \
  -i openapi.yaml \
  -g python \
  -o ./python-client
While OpenAPI Generator can create clients, Express Zod API’s type-safe client generator provides superior TypeScript integration.

Best Practices

1. Use Descriptive Names

// Good
const getUserByIdEndpoint = factory.build({ /* ... */ });

// Less good
const endpoint1 = factory.build({ /* ... */ });

2. Provide Examples

z.string().example("example-value")
  .describe("Clear description of what this is")

3. Tag Your Endpoints

factory.build({
  tag: "users",
  // ...
});

4. Add Deprecation Warnings

factory.build({
  deprecated: true, // Marks endpoint as deprecated
  input: z.object({
    oldField: z.string().deprecated(), // Marks field as deprecated
  }),
});

5. Use Component Mode for Large APIs

new Documentation({
  composition: "components", // Reduces duplication
  // ...
});

Validation

Validate your generated OpenAPI spec:
# Using swagger-cli
npm install -g @apidevtools/swagger-cli
swagger-cli validate openapi.yaml

# Using openapi-cli
npm install -g @redocly/cli
openapi lint openapi.yaml

Next Steps