Skip to content

@nodalite/openapi

OpenAPI 3.1.0 spec generation, Swagger UI, and ReDoc for Nodalite — auto-document your API, just like FastAPI.

bash
npm install @nodalite/openapi
ts
import { openapi } from '@nodalite/openapi';

Peer dependency: zod (optional — only needed if using Zod schemas for request/response metadata).

Quick Start

ts
import { App } from '@nodalite/core';
import { openapi } from '@nodalite/openapi';
import { serve } from '@nodalite/adapter-node';
import { z } from 'zod';

const app = new App();
const api = openapi(app, {
  info: { title: 'My API', version: '1.0.0' },
});

api.get('/users', handler, {
  openapi: {
    summary: 'List all users',
    tags: ['Users'],
    responses: {
      200: { description: 'A list of users' },
    },
  },
});

serve(app, { port: 3000 });
  • GET /openapi.json — the generated OpenAPI spec
  • GET /swagger — Swagger UI
  • GET /redoc — ReDoc

openapi(app, options)

Factory function that wraps an existing App and returns an OpenAPIApp.

ts
import { openapi } from '@nodalite/openapi';

const api = openapi(app, {
  info: { title: 'My API', version: '1.0.0' },
  servers: [{ url: 'https://api.example.com' }],
});

Options

OptionTypeDefaultDescription
infoOpenAPIInfoAPI title, version, description, terms of service
serversOpenAPIServer[][]Server entries for the spec
specPathstring"/openapi.json"Path for the JSON spec endpoint
docsPathstring"/swagger"Path for the Swagger UI endpoint
redocPathstring"/redoc"Path for the ReDoc endpoint

OpenAPIInfo

PropertyTypeRequiredDescription
titlestringyesAPI title
versionstringyesAPI version string
descriptionstringnoAPI description
termsOfServicestringnoTerms of service URL

OpenAPIServer

PropertyTypeRequiredDescription
urlstringyesServer URL
descriptionstringnoServer description

OpenAPIApp

Mirrors the App API so adding OpenAPI metadata is a drop-in wrapper, not a rewrite. Every route method accepts an optional opts parameter with an openapi field for metadata.

Route Methods

ts
api.get(path, handler, opts?)
api.post(path, handler, opts?)
api.put(path, handler, opts?)
api.patch(path, handler, opts?)
api.delete(path, handler, opts?)
api.all(path, handler, opts?)

RouteOptions

OptionTypeDescription
middlewaresMiddleware[]Route-scoped middleware
openapiRouteOpenAPIMetaOpenAPI metadata for this route

RouteOpenAPIMeta

PropertyTypeDescription
summarystringShort summary of the operation
descriptionstringDetailed description
operationIdstringUnique operation identifier
tagsstring[]Tags for grouping in the UI
deprecatedbooleanMark the operation as deprecated
requestOpenAPIRequestMetaRequest schema metadata
responsesRecord<number, OpenAPIResponseMeta>Response schemas by status code

use(middleware) / use(path, middleware)

Register middleware on the underlying app.

ts
api.use(cors());
api.use('/api/*', authMiddleware);

group(prefix, build)

Register documented route groups under a shared prefix.

ts
api.group('/users', (g) => {
  g.get('/', listHandler, {
    openapi: { summary: 'List users', tags: ['Users'] },
  });
  g.post('/', createHandler, {
    openapi: { summary: 'Create user', tags: ['Users'] },
  });
});

onError(handler) / notFound(handler)

Delegate to the underlying app's error handling.

Request Metadata

The request field in RouteOpenAPIMeta describes the shape of incoming requests. Schemas can be Zod v3, Zod v4, raw JSON Schema objects, or anything with a toJSONSchema() method.

ts
import { z } from 'zod';

api.post('/users', createUser, {
  openapi: {
    summary: 'Create a user',
    tags: ['Users'],
    request: {
      body: z.object({
        name: z.string(),
        email: z.string().email(),
      }),
      query: z.object({
        notify: z.boolean().optional(),
      }),
      params: z.object({
        orgId: z.string(),
      }),
      headers: z.object({
        'x-api-key': z.string(),
      }),
    },
    responses: {
      201: {
        description: 'User created',
        schema: z.object({ id: z.string(), name: z.string() }),
      },
    },
  },
});

OpenAPIRequestMeta

PropertyTypeDescription
bodyunknownRequest body schema (JSON)
queryunknownQuery parameter schema
paramsunknownPath parameter schema
headersunknownRequest header schema

OpenAPIResponseMeta

PropertyTypeDescription
descriptionstringResponse description (required)
schemaunknownResponse body schema

Path parameters (:param style) are auto-extracted and converted to OpenAPI {param} style in the spec. Query and header schemas are converted to individual parameter entries with the correct in field.

Schema Conversion

toOpenAPISchema(schema) converts supported schema types to OpenAPI-compatible JSON Schema. This function is exported for advanced use cases.

Supported Schema Types

Zod v3 (_def.typeName-based):

Zod TypeOpenAPI Schema
ZodString{ type: "string" }
ZodNumber{ type: "number" }
ZodBoolean{ type: "boolean" }
ZodNull{ type: "null" }
ZodDate{ type: "string", format: "date-time" }
ZodLiteral{ type, const }
ZodEnum{ type: "string", enum }
ZodObject{ type: "object", properties, required }
ZodArray{ type: "array", items }
ZodOptionalInner schema
ZodNullable{ anyOf: [inner, { type: "null" }] }
ZodUnion / ZodDiscriminatedUnion{ oneOf }
ZodRecord{ type: "object", additionalProperties }
ZodDefaultInner schema

Zod v4 (_def.type-based):

Zod TypeOpenAPI Schema
string{ type: "string" } with minLength/maxLength/pattern
number / integer{ type: "number" } with minimum/maximum
boolean{ type: "boolean" }
object{ type: "object", properties, required }
array{ type: "array", items }
enum{ type: "string", enum }
union{ oneOf }
intersection{ allOf }
record{ type: "object", additionalProperties }
tuple{ type: "array" }
literal{ type, enum }

Other:

  • Zod schemas with toJSONSchema() — passthrough (strips $schema key)
  • Raw JSON Schema objects — passed through directly

generateSpec(routes, options)

Low-level function that generates an OpenAPI 3.1.0 document from stored route metadata. Used internally by OpenAPIApp — exported for testing or custom integrations.

ts
import { generateSpec } from '@nodalite/openapi';

const doc = generateSpec(routes, {
  info: { title: 'My API', version: '1.0.0' },
});
// doc is an OpenAPIDocument object

Behavior

  • Converts :param route paths to {param} OpenAPI style
  • Extracts path/query/header parameters from schemas
  • Request body schemas are registered in components.schemas with $ref deduplication — identical schemas are referenced, not duplicated
  • Schema names are auto-generated from property names (e.g., { name, email }NameAndEmail)

swaggerUIHTML(specURL, title) / redocHTML(specURL, title)

Generate self-contained HTML pages for Swagger UI and ReDoc. Used internally to serve the docs endpoints. Exported for custom integrations (e.g., serving from a different path or embedding in an existing page).

ts
import { swaggerUIHTML, redocHTML } from '@nodalite/openapi';

const html = swaggerUIHTML('/openapi.json', 'My API');
// Serve this HTML with content-type: text/html

Types

ts
import type {
  OpenAPIInfo,
  OpenAPIServer,
  OpenAPIOptions,
  OpenAPIRequestMeta,
  OpenAPIResponseMeta,
  RouteOpenAPIMeta,
  RouteOptions,
  StoredRoute,
  OpenAPIDocument,
  OpenAPIPathItem,
  OpenAPIParameter,
  OpenAPIRequestBody,
  OpenAPIResponse,
  OpenAPISchema,
} from '@nodalite/openapi';

OpenAPIDocument

The top-level OpenAPI 3.1.0 document structure.

PropertyTypeDescription
openapistringAlways "3.1.0"
infoOpenAPIInfoAPI information
serversOpenAPIServer[]Server entries
pathsRecord<string, Record<string, OpenAPIPathItem>>Path → method → operation
components{ schemas: Record<string, OpenAPISchema> }Reusable schema definitions

Full Example

ts
import { App } from '@nodalite/core';
import { openapi } from '@nodalite/openapi';
import { cors, securityHeaders, jwtAuth } from '@nodalite/middleware';
import { serve } from '@nodalite/adapter-node';
import { z } from 'zod';

const app = new App();
app.use('*', securityHeaders());
app.use('*', cors({ origin: 'https://your-frontend.example' }));

const api = openapi(app, {
  info: {
    title: 'Nodalite API',
    version: '1.0.0',
    description: 'A runtime-agnostic API with auto-generated documentation.',
  },
  servers: [
    { url: 'http://localhost:3000', description: 'Development' },
  ],
});

const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(['admin', 'user']),
});

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  role: z.string(),
});

api.group('/users', (g) => {
  g.get('/', async (c) => {
    return c.json([{ id: '1', name: 'Alice', email: 'alice@example.com', role: 'admin' }]);
  }, {
    openapi: {
      summary: 'List all users',
      operationId: 'listUsers',
      tags: ['Users'],
      responses: {
        200: { description: 'A list of users', schema: z.array(UserSchema) },
      },
    },
  });

  g.post('/', async (c) => {
    const body = await c.req.json();
    return c.json({ id: '2', ...body }, 201);
  }, {
    middlewares: [jwtAuth({ secret: process.env.JWT_SECRET! })],
    openapi: {
      summary: 'Create a user',
      operationId: 'createUser',
      tags: ['Users'],
      request: {
        body: CreateUserSchema,
      },
      responses: {
        201: { description: 'User created', schema: UserSchema },
      },
    },
  });

  g.get('/:id', async (c) => {
    return c.json({ id: c.req.param('id'), name: 'Alice', email: 'alice@example.com', role: 'admin' });
  }, {
    openapi: {
      summary: 'Get a user by ID',
      operationId: 'getUser',
      tags: ['Users'],
      request: {
        params: z.object({ id: z.string() }),
      },
      responses: {
        200: { description: 'User found', schema: UserSchema },
        404: { description: 'User not found' },
      },
    },
  });
});

serve(app, { port: 3000 });

Visit:

  • http://localhost:3000/openapi.json — raw OpenAPI spec
  • http://localhost:3000/swagger — interactive Swagger UI
  • http://localhost:3000/redoc — clean ReDoc documentation

Released under the MIT License.