Skip to main content

OpenAPI Documentation

Codehooks provides built-in OpenAPI 3.0 documentation that automatically generates interactive API docs from your code. When combined with crudlify, your schemas are automatically converted to OpenAPI specifications and served via Swagger UI.

After deployment, visit:

  • /docs — Interactive Swagger UI (supports authentication with API tokens)
  • /openapi.json — Raw OpenAPI 3.0 specification

app.openapi(config, swaggerPath?)

Enable automatic OpenAPI 3.0 documentation generation and serve an interactive Swagger UI.

Parameters

  • config (object): OpenAPI configuration object
    • info (object, required): API metadata
      • title (string): API title
      • version (string): API version
      • description (string, optional): API description
    • servers (array, optional): Server URLs for different environments
    • tags (array, optional): Tags to organize endpoints
    • filter (function, optional): Filter which operations appear in docs (op) => boolean
    • specPath (string, optional): Path for the JSON spec (default: /openapi.json)
    • externalDocs (object, optional): Link to external documentation
    • security (array, optional): Security schemes
    • components (object, optional): Additional component schemas
  • swaggerPath (string, optional): Path for Swagger UI (default: /docs)

Returns void

Code example

import { app } from 'codehooks-js';
import { z } from 'zod';

const todoSchema = z.object({
title: z.string().min(1).max(100),
completed: z.boolean().default(false)
});

// Enable OpenAPI docs
app.openapi({
info: { title: 'Todo API', version: '1.0.0' }
});

// Auto-generate CRUD endpoints with schema validation
app.crudlify({ todos: todoSchema });

export default app.init();

openapi(spec) middleware

Add OpenAPI metadata to custom routes. Use this middleware to document individual endpoints that are not auto-generated by crudlify.

Parameters

  • spec (object): OpenAPI operation specification
    • summary (string): Short description of the endpoint
    • description (string, optional): Detailed description
    • tags (array): Tags to group the endpoint
    • requestBody (object, optional): Request body schema (supports Zod/Yup schemas directly)
    • parameters (array, optional): Query/path parameter definitions
    • responses (object): Response definitions by status code
    • security (array, optional): Security requirements (use [] for public endpoints)

Returns Middleware function

Code example

import { app, openapi } from 'codehooks-js';

app.openapi({
info: { title: 'My API', version: '1.0.0' }
});

app.get('/health',
openapi({
summary: 'Health check',
tags: ['System'],
responses: {
200: { description: 'Service is healthy' }
}
}),
(req, res) => res.json({ status: 'ok' })
);

export default app.init();

Configuration Options

Full configuration example with all available options:

app.openapi({
// API metadata (required)
info: {
title: 'My API',
version: '1.0.0',
description: 'API description'
},

// Custom path for the spec (default: '/openapi.json')
specPath: '/openapi.json',

// Server URLs
servers: [
{ url: 'https://myproject.api.codehooks.io/dev', description: 'Development' },
{ url: 'https://myproject.api.codehooks.io/prod', description: 'Production' }
],

// Filter which operations appear in docs
filter: (op) => op.method !== 'delete',

// Global tags
tags: [
{ name: 'Todos', description: 'Todo operations' }
],

// External documentation link
externalDocs: {
description: 'Full documentation',
url: 'https://docs.example.com'
},

// Security schemes
security: [{ apiKey: [] }],

// Additional component schemas
components: {
schemas: {
Error: {
type: 'object',
properties: {
message: { type: 'string' }
}
}
}
}
}, '/docs'); // Second parameter: Swagger UI path (default: '/docs')

Filter Function

Use the filter function to control which operations appear in the documentation.

Parameters passed to filter

PropertyTypeDescription
op.methodstringHTTP method: 'get', 'post', 'put', 'patch', 'delete'
op.pathstringRoute path: '/todos', '/todos/{ID}'
op.tagsarrayArray of tags: ['Todos']
op.summarystringOperation summary
op.operationIdstringOperation ID

Code examples

// Exclude DELETE operations
filter: (op) => op.method !== 'delete'

// Exclude batch operations
filter: (op) => !op.path.includes('_byquery')

// Exclude internal routes
filter: (op) => !op.path.startsWith('/internal')

// Only include specific tags
filter: (op) => op.tags.includes('Public')

// Combine conditions
filter: (op) => {
if (op.method === 'delete') return false;
if (op.path.startsWith('/admin')) return false;
return true;
}

Schema Support

OpenAPI documentation automatically converts schemas from Zod, Yup, or JSON Schema.

Zod

import { z } from 'zod';

const userSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().min(0).max(150).optional(),
role: z.enum(['admin', 'user']).default('user'),
active: z.boolean().default(true)
});

app.crudlify({ users: userSchema });

Generated OpenAPI schema:

{
"type": "object",
"properties": {
"name": { "type": "string", "minLength": 1, "maxLength": 100 },
"email": { "type": "string", "format": "email" },
"age": { "type": "number", "minimum": 0, "maximum": 150 },
"role": { "type": "string", "enum": ["admin", "user"], "default": "user" },
"active": { "type": "boolean", "default": true }
},
"required": ["name", "email"]
}

Yup

import * as yup from 'yup';

const userSchema = yup.object({
name: yup.string().min(1).max(100).required(),
email: yup.string().email().required(),
age: yup.number().min(0).max(150),
role: yup.string().oneOf(['admin', 'user']).default('user'),
active: yup.boolean().default(true)
});

app.crudlify({ users: userSchema });

JSON Schema

JSON Schema validation works out of the box using ajv (available in Codehooks runtime).

const userSchema = {
type: 'object',
required: ['name', 'email'],
properties: {
name: {
type: 'string',
minLength: 1,
maxLength: 100,
description: 'Full name of the user'
},
email: {
type: 'string',
format: 'email',
description: 'Email address'
},
age: {
type: 'integer',
minimum: 0,
maximum: 150,
description: 'Age in years'
},
active: {
type: 'boolean',
default: true,
description: 'Account status'
}
}
};

app.crudlify({ users: userSchema }, { schema: 'json-schema' });

Adding Field Descriptions

Add descriptions to schema fields for better API documentation.

Zod — Use .describe()

import { z } from 'zod';

const todoSchema = z.object({
title: z.string()
.min(1)
.max(100)
.describe('Title of the todo item'),

completed: z.boolean()
.default(false)
.describe('Whether the todo is completed'),

priority: z.number()
.min(1)
.max(5)
.optional()
.describe('Priority level from 1 (low) to 5 (high)')
});

Yup — Use .meta()

import * as yup from 'yup';

const todoSchema = yup.object({
title: yup.string()
.min(1)
.max(100)
.required()
.meta({ description: 'Title of the todo item' }),

completed: yup.boolean()
.default(false)
.meta({ description: 'Whether the todo is completed' }),

priority: yup.number()
.min(1)
.max(5)
.meta({ description: 'Priority level from 1 (low) to 5 (high)' })
});

Crudlify Integration

When using crudlify, OpenAPI documentation is automatically generated for all CRUD operations:

MethodPathOperation
POST/{collection}Create document
GET/{collection}List documents
GET/{collection}/{ID}Get document by ID
PUT/{collection}/{ID}Replace document
PATCH/{collection}/{ID}Update document
DELETE/{collection}/{ID}Delete document
PATCH/{collection}/_byqueryBatch update
DELETE/{collection}/_byqueryBatch delete

Multiple collections

app.crudlify({
users: userSchema,
posts: postSchema,
comments: commentSchema
});

Schema-less collections

Collections without a schema accept any valid JSON:

app.crudlify({
users: userSchema, // Validated
logs: null // Accepts any JSON
});

Documenting Custom Routes

Use the openapi() middleware to document custom routes with full OpenAPI specifications.

Basic example

import { app, openapi } from 'codehooks-js';

app.get('/health',
openapi({
summary: 'Health check',
tags: ['System'],
responses: {
200: { description: 'Service is healthy' }
}
}),
(req, res) => res.json({ status: 'ok' })
);

With request body

app.post('/upload',
openapi({
summary: 'Upload file',
tags: ['Files'],
requestBody: {
required: true,
content: {
'multipart/form-data': {
schema: {
type: 'object',
properties: {
file: { type: 'string', format: 'binary' }
}
}
}
}
},
responses: {
201: { description: 'File uploaded' },
400: { description: 'Invalid file' }
}
}),
async (req, res) => {
// Handle upload
}
);

With query parameters

app.get('/users',
openapi({
summary: 'List users',
tags: ['Users'],
parameters: [
{ name: 'role', in: 'query', schema: { type: 'string', enum: ['admin', 'user'] } },
{ name: 'limit', in: 'query', schema: { type: 'integer', default: 20 } }
],
responses: {
200: { description: 'List of users' }
}
}),
async (req, res) => {
res.json([]);
}
);

Using Zod schema in requestBody

import { z } from 'zod';

const UserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email()
});

app.post('/users',
openapi({
summary: 'Create user',
tags: ['Users'],
requestBody: {
required: true,
content: {
'application/json': {
schema: UserSchema // Zod schema auto-converts to JSON Schema
}
}
},
responses: {
201: { description: 'User created' },
400: { description: 'Validation error' }
}
}),
async (req, res) => {
res.status(201).json(req.body);
}
);

Authentication

By default, all endpoints require the x-apikey header. The Swagger UI includes an authorize dialog where you can enter your API token to test authenticated endpoints.

Mark endpoint as public

app.get('/public-endpoint',
openapi({
summary: 'Public endpoint',
security: [] // No authentication required
}),
(req, res) => res.json({ message: 'Hello!' })
);

// Also bypass auth middleware
app.auth('/public-endpoint', (req, res, next) => next());

Swagger UI Path

The Swagger UI path defaults to /docs. You can customize it:

app.openapi({
info: { title: 'My API', version: '1.0.0' }
}, '/api-docs'); // Swagger UI at /api-docs

Complete Example

Full example with multiple collections, custom endpoints, validation middleware, and OpenAPI configuration:

import { app, Datastore, openapi } from 'codehooks-js';
import { z } from 'zod';

// ============================================
// Schema Definitions
// ============================================

const UserSchema = z.object({
name: z.string()
.min(1, 'Name is required')
.max(100, 'Name too long')
.describe('Full name of the user'),
email: z.string()
.email('Invalid email format')
.describe('Email address (must be unique)'),
age: z.number()
.int('Age must be an integer')
.min(0, 'Age cannot be negative')
.max(150, 'Age seems unrealistic')
.optional()
.describe('Age in years'),
role: z.enum(['admin', 'user', 'guest'])
.default('user')
.describe('User role for access control')
});

const UpdateUserSchema = UserSchema.partial();

// ============================================
// Validation Middleware
// ============================================

function validate(schema) {
return async (req, res, next) => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
if (error.issues) {
return res.status(400).json({
error: 'Validation failed',
details: error.issues.map(e => ({
field: e.path.join('.'),
message: e.message
}))
});
}
return res.status(400).json({ error: 'Invalid request body' });
}
};
}

// ============================================
// OpenAPI Configuration
// ============================================

app.openapi({
info: {
title: 'User Management API',
version: '1.0.0',
description: 'API with Zod validation and auto-generated OpenAPI docs'
},
servers: [
{ url: 'https://myapi.api.codehooks.io/dev', description: 'Development' },
{ url: 'https://myapi.api.codehooks.io/prod', description: 'Production' }
],
tags: [
{ name: 'Users', description: 'User management operations' },
{ name: 'System', description: 'System endpoints' }
],
filter: (op) => !op.path.includes('_byquery')
});

// ============================================
// Custom Routes
// ============================================

// Health check (public)
app.get('/health',
openapi({
summary: 'Health check',
tags: ['System'],
security: [],
responses: {
200: {
description: 'Service status',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
status: { type: 'string', example: 'ok' },
timestamp: { type: 'string', format: 'date-time' }
}
}
}
}
}
}
}),
(req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
}
);
app.auth('/health', (req, res, next) => next());

// Create user
app.post('/users',
openapi({
summary: 'Create a new user',
description: 'Email must be unique. Returns 409 if email exists.',
tags: ['Users'],
requestBody: {
required: true,
content: {
'application/json': {
schema: UserSchema
}
}
},
responses: {
201: { description: 'User created successfully' },
400: { description: 'Validation error' },
409: { description: 'Email already exists' }
}
}),
validate(UserSchema),
async (req, res) => {
const db = await Datastore.open();
const existing = await db.findOneOrNull('users', { email: req.body.email });
if (existing) {
return res.status(409).json({ error: 'Email already exists' });
}
const user = await db.insertOne('users', {
...req.body,
createdAt: new Date().toISOString()
});
res.status(201).json(user);
}
);

// Get user by ID
app.get('/users/:id',
openapi({
summary: 'Get user by ID',
tags: ['Users'],
responses: {
200: { description: 'User found' },
404: { description: 'User not found' }
}
}),
async (req, res) => {
const db = await Datastore.open();
const user = await db.findOneOrNull('users', req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
}
);

// Update user
app.patch('/users/:id',
openapi({
summary: 'Update user',
tags: ['Users'],
requestBody: {
required: true,
content: {
'application/json': {
schema: UpdateUserSchema
}
}
},
responses: {
200: { description: 'User updated' },
400: { description: 'Validation error' },
404: { description: 'User not found' }
}
}),
validate(UpdateUserSchema),
async (req, res) => {
const db = await Datastore.open();
const exists = await db.findOneOrNull('users', req.params.id);
if (!exists) {
return res.status(404).json({ error: 'User not found' });
}
const user = await db.updateOne('users', req.params.id, {
...req.body,
updatedAt: new Date().toISOString()
});
res.json(user);
}
);

// Delete user
app.delete('/users/:id',
openapi({
summary: 'Delete user',
tags: ['Users'],
responses: {
200: { description: 'User deleted' },
404: { description: 'User not found' }
}
}),
async (req, res) => {
const db = await Datastore.open();
const exists = await db.findOneOrNull('users', req.params.id);
if (!exists) {
return res.status(404).json({ error: 'User not found' });
}
await db.removeOne('users', req.params.id);
res.json({ message: 'User deleted', _id: req.params.id });
}
);

// List users
app.get('/users',
openapi({
summary: 'List users',
tags: ['Users'],
parameters: [
{ name: 'role', in: 'query', schema: { type: 'string', enum: ['admin', 'user', 'guest'] } },
{ name: 'limit', in: 'query', schema: { type: 'integer', default: 20 } }
],
responses: {
200: { description: 'List of users' }
}
}),
async (req, res) => {
const db = await Datastore.open();
const query = req.query.role ? { role: req.query.role } : {};
const users = await db.getMany('users', query, {
limit: parseInt(req.query.limit) || 20
}).toArray();
res.json(users);
}
);

export default app.init();

Quick Reference

ConceptImplementation
Enable OpenAPIapp.openapi({ info: { title, version } })
Custom Swagger pathapp.openapi(config, '/api-docs')
Document custom routeapp.get('/path', openapi({ ... }), handler)
Use Zod schemaPass schema directly to requestBody.content['application/json'].schema
Add field descriptionsZod: .describe(), Yup: .meta({ description })
Public endpointsecurity: [] in openapi spec
Filter operationsfilter: (op) => op.method !== 'delete'

Response Codes

CodeWhen Used
200Successful GET, PATCH, DELETE
201Successful POST (created)
400Validation failed
404Resource not found
409Duplicate resource (conflict)