OpenAPI
Lacis generates an OpenAPI 3.1.0 spec at runtime from your registered routes. Routes wrapped with defineHandler contribute their schema and meta fields to the spec.
Add an openapi key to your server config:
// server.tsimport { createServer } from 'lacis'
createServer('./routes', { openapi: { path: '/openapi.json', info: { title: 'My API', version: '1.0.0', }, servers: [ { url: 'https://api.example.com', description: 'production' }, { url: 'http://localhost:3000', description: 'local' }, ], },})The spec is served at /openapi.json on every request. No build step required.
Response schemas
Section titled “Response schemas”Declaring responses on a route both types the
handler and documents each status code in the spec — one source of truth.
export const GET = defineHandler({ responses: { 200: z.object({ id: z.string(), name: z.string() }), 404: z.object({ error: z.string() }), }, handler: async (req, res) => res.status(200).json({ id: '1', name: 'Ada' }),})Schema converters
Section titled “Schema converters”The spec is built by converting your validator schemas to JSON Schema. Each library requires its own converter:
| Library | Package to install |
|---|---|
| Zod 4.4+ | none (native toJSONSchema) |
| Zod < 4.4 | zod-to-json-schema |
| Valibot | @valibot/to-json-schema |
| ArkType | none (native .toJsonSchema()) |
Install only the converter that matches your validator.
Annotating routes with meta
Section titled “Annotating routes with meta”// routes/users/[id]/index.tsimport { defineHandler } from 'lacis'import { z } from 'zod'
export const GET = defineHandler({ params: z.object({ id: z.string().uuid() }), meta: { summary: 'Get user by ID', description: 'Returns a single user record.', tags: ['users'], }, handler: async (req, res) => { res.json({ id: req.params.id }) },})
export const DELETE = defineHandler({ params: z.object({ id: z.string().uuid() }), meta: { summary: 'Delete user', tags: ['users'], deprecated: true }, handler: async (req, res) => { res.status(204).send('') },})Using the spec
Section titled “Using the spec”The generated JSON is standard OpenAPI 3.1.0 — drop it into any compatible tool.
Scalar (recommended):
<!doctype html><html> <head><title>API Reference</title></head> <body> <script id="api-reference" data-url="/openapi.json" src="https://cdn.jsdelivr.net/npm/@scalar/api-reference" ></script> </body></html>Postman / Insomnia: use “Import from URL” → http://localhost:3000/openapi.json.
OpenAPI Generator: generate clients, SDKs, or server stubs in 50+ languages from the spec.
# Installnpm install @openapitools/openapi-generator-cli -g
# Generate a TypeScript fetch clientopenapi-generator-cli generate \ -i http://localhost:3000/openapi.json \ -g typescript-fetch \ -o ./generated/clientSee the OpenAPI Generator docs for all supported generators and options.
Typed client with openapi-fetch
Section titled “Typed client with openapi-fetch”Lacis ships no client codegen on purpose — mature tools do it better, in many languages,
and writing one would betray the zero-dependency ethos. For an end-to-end typed TypeScript
client with zero maintained code, point openapi-fetch
at your spec:
npx openapi-typescript http://localhost:3000/openapi.json -o ./src/api.d.tsimport createClient from 'openapi-fetch'import type { paths } from './api'
const api = createClient<paths>({ baseUrl: 'http://localhost:3000' })
// path, params, body and response are all typed from your specconst { data, error } = await api.GET('/users/{id}', { params: { path: { id: '1' } },})Routes without defineHandler
Section titled “Routes without defineHandler”Plain handlers appear in the spec with a minimal operation. Add defineHandler + meta to get richer output.