Configuration

The openapi() function takes your routes and a config object, and returns spec and docs route handlers.

import { setup, route } from "@hectoday/http";
import { openapi } from "@hectoday/openapi";

const routes = [
  /* ... your routes ... */
];

const api = openapi(routes, {
  info: {
    title: "Book Catalog API",
    version: "2.0.0",
    description: "A catalog of books, authors, and reviews.",
  },
  servers: [
    { url: "http://localhost:3000", description: "Development" },
    { url: "https://api.example.com", description: "Production" },
  ],
  security: [{ bearerAuth: [] }],
  securitySchemes: {
    bearerAuth: {
      type: "http",
      scheme: "bearer",
      bearerFormat: "JWT",
    },
  },
  tags: [
    { name: "Books", description: "Book catalog management" },
    { name: "Reviews", description: "Book reviews and ratings" },
  ],
});

const app = setup({
  routes: [
    ...routes,
    api.spec(route), // serves /openapi.json
    api.docs(route), // serves /docs (Scalar UI)
  ],
});

Function signature

openapi(routes: RouteDescriptor[], config: OpenApiConfig): OpenApiResult

The first argument is your routes array. The function reads the Zod schemas on each route to generate the spec. The second argument is the config.

Return value

interface OpenApiResult {
  spec: (route) => RouteDescriptor; // GET /openapi.json
  docs: (route) => RouteDescriptor; // GET /docs (Scalar UI)
}

Both methods take the route object and return a route descriptor that you add to your routes array.

Config options

info

Type: { title: string; version: string; description?: string; license?: { name: string; url?: string } }Required

API metadata. Appears at the top of the generated spec and documentation UI.

info: {
  title: "Book Catalog API",
  version: "2.0.0",
  description: "A catalog of books, authors, and reviews.",
},

servers

Type: Array<{ url: string; description?: string }> — Optional

Base URLs for the API. Documentation UIs show a dropdown to select the server.

servers: [
  { url: "http://localhost:3000", description: "Development" },
  { url: "https://staging-api.example.com", description: "Staging" },
  { url: "https://api.example.com", description: "Production" },
],

security

Type: Array<Record<string, string[]>> — Optional

Default security requirements for all endpoints.

security: [{ bearerAuth: [] }],

securitySchemes

Type: Record<string, SecurityScheme> — Optional

Authentication scheme definitions.

securitySchemes: {
  bearerAuth: {
    type: "http",
    scheme: "bearer",
    bearerFormat: "JWT",
    description: "JWT token from POST /auth/login",
  },
  apiKey: {
    type: "apiKey",
    in: "header",
    name: "X-API-Key",
  },
},

tags

Type: Array<{ name: string; description?: string }> — Optional

Grouping labels for endpoints. The order determines display order in the documentation UI.

tags: [
  { name: "Books", description: "Book catalog management" },
  { name: "Reviews", description: "Book reviews and ratings" },
  { name: "Auth", description: "Authentication endpoints" },
],

specPath

Type: string — Default: "/openapi.json"

The URL path where the spec is served.

openapi(routes, {
  info: { title: "My API", version: "1.0.0" },
  specPath: "/api/spec.json",
});

docsPath

Type: string — Default: "/docs"

The URL path where the Scalar documentation UI is served.

openapi(routes, {
  info: { title: "My API", version: "1.0.0" },
  docsPath: "/reference",
});

Generated spec

The generated spec follows OpenAPI 3.1. Zod schemas on request are converted to JSON Schema for the spec. Path parameters in the :param format are converted to the OpenAPI {param} format.

Routes without any schemas still appear in the spec with a default 200 OK response.

Zod to JSON Schema mapping

ZodJSON Schema
z.string(){ "type": "string" }
z.string().min(1).max(200){ "type": "string", "minLength": 1, "maxLength": 200 }
z.url(){ "type": "string", "format": "uri" }
z.uuid(){ "type": "string", "format": "uuid" }
z.email(){ "type": "string", "format": "email" }
z.number(){ "type": "number" }
z.number().int(){ "type": "integer" }
z.number().min(1).max(5){ "type": "number", "minimum": 1, "maximum": 5 }
z.boolean(){ "type": "boolean" }
z.enum(["a", "b"]){ "type": "string", "enum": ["a", "b"] }
z.array(z.string()){ "type": "array", "items": { "type": "string" } }
z.object({ ... }){ "type": "object", "properties": { ... }, "required": [...] }
.optional()Field not in required array
.nullable(){ "type": ["string", "null"] }
.default("x"){ "default": "x" }