DocTreen
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

  1. defineRoute declaration
  2. JSDoc inside the handler function
  3. Runtime traffic sampling (development only)

Declared schemas always win.

How it works

  1. expressAdapter(app, config) returns a middleware registered at docsPath.
  2. On the first request to /docs, DocTreen walks app._router.stack recursively to discover all registered routes — lazy introspection solves the middleware-before-routes ordering problem.
  3. Route handlers are wrapped so that, in development, real HTTP traffic can fill in request/response schemas that weren't declared via defineRoute or JSDoc. Declared schemas always win; runtime sampling only fills the gaps.
  4. JSDoc comments inside handler functions are parsed via fn.toString() at runtime.
  5. 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.
  6. If flows are configured, POST /docs/__flows/run executes them through the shared runner.

On this page