Adapters
Express
Drop-in middleware for Express 4 and 5.
const express = require('express');
const { expressAdapter } = require('doctreen/express');
const app = express();
app.use(express.json());
app.get('/users', (req, res) => res.json([]));
app.post('/users', (req, res) => res.status(201).json({ id: 1 }));
// Mount after your routes
app.use(expressAdapter(app, {
meta: { title: 'My API', version: '1.0.0' },
}));
app.listen(3000, () => console.log('Docs at http://localhost:3000/docs'));Mount order
Mount expressAdapter after your routes. The adapter walks
app._router.stack lazily on the first docs request, so anything registered
before that point shows up.
With defineRoute
const { defineRoute } = require('doctreen/express');
const { z } = require('zod');
app.post('/users', defineRoute(
(req, res) => res.status(201).json({ id: 1, ...req.body }),
{
description: 'Create a user',
request: { body: z.object({ name: z.string(), email: z.string().email() }) },
response: z.object({ id: z.number(), name: z.string(), email: z.string() }),
errors: { 409: 'Email already in use' },
}
));See defineRoute for the full schema reference.
Schema resolution order
defineRoutedeclaration- JSDoc inside the handler function
- Runtime traffic sampling (development only)
Declared schemas always win.
How it works
expressAdapter(app, config)returns a middleware registered atdocsPath.- On the first request to
/docs, DocTreen walksapp._router.stackrecursively to discover all registered routes — lazy introspection solves the middleware-before-routes ordering problem. - Route handlers are wrapped so that, in development, real HTTP traffic can fill in request/response schemas that weren't declared via
defineRouteor JSDoc. Declared schemas always win; runtime sampling only fills the gaps. - JSDoc comments inside handler functions are parsed via
fn.toString()at runtime. - If a route's schema was declared, each incoming payload is also compared against it for schema drift — sampled, aggregated, and exposed via the UI tab and
/drift.json. - If flows are configured,
POST /docs/__flows/runexecutes them through the shared runner.