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
- info (object, required): API metadata
- 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
| Property | Type | Description |
|---|---|---|
op.method | string | HTTP method: 'get', 'post', 'put', 'patch', 'delete' |
op.path | string | Route path: '/todos', '/todos/{ID}' |
op.tags | array | Array of tags: ['Todos'] |
op.summary | string | Operation summary |
op.operationId | string | Operation 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:
| Method | Path | Operation |
|---|---|---|
| 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}/_byquery | Batch update |
| DELETE | /{collection}/_byquery | Batch 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
| Concept | Implementation |
|---|---|
| Enable OpenAPI | app.openapi({ info: { title, version } }) |
| Custom Swagger path | app.openapi(config, '/api-docs') |
| Document custom route | app.get('/path', openapi({ ... }), handler) |
| Use Zod schema | Pass schema directly to requestBody.content['application/json'].schema |
| Add field descriptions | Zod: .describe(), Yup: .meta({ description }) |
| Public endpoint | security: [] in openapi spec |
| Filter operations | filter: (op) => op.method !== 'delete' |
Response Codes
| Code | When Used |
|---|---|
| 200 | Successful GET, PATCH, DELETE |
| 201 | Successful POST (created) |
| 400 | Validation failed |
| 404 | Resource not found |
| 409 | Duplicate resource (conflict) |