Skip to content

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.ts
import { 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.

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' }),
})

The spec is built by converting your validator schemas to JSON Schema. Each library requires its own converter:

LibraryPackage to install
Zod 4.4+none (native toJSONSchema)
Zod < 4.4zod-to-json-schema
Valibot@valibot/to-json-schema
ArkTypenone (native .toJsonSchema())

Install only the converter that matches your validator.

// routes/users/[id]/index.ts
import { 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('')
},
})

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.

Terminal window
# Install
npm install @openapitools/openapi-generator-cli -g
# Generate a TypeScript fetch client
openapi-generator-cli generate \
-i http://localhost:3000/openapi.json \
-g typescript-fetch \
-o ./generated/client

See the OpenAPI Generator docs for all supported generators and options.

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:

Terminal window
npx openapi-typescript http://localhost:3000/openapi.json -o ./src/api.d.ts
import 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 spec
const { data, error } = await api.GET('/users/{id}', {
params: { path: { id: '1' } },
})

Plain handlers appear in the spec with a minimal operation. Add defineHandler + meta to get richer output.