npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

doctreen

v1.4.3

Published

Auto-generates and serves API documentation for Node.js backends via middleware

Downloads

193

Readme

DocTreen

Auto-generate and serve interactive API documentation for Express, Fastify, Hono, Koa, and NestJS backends — with zero runtime dependencies and first-class Zod support.

DocTreen introspects your app at startup or first request, reads inline JSDoc or decorator metadata, and serves a self-contained interactive docs UI at the configured docsPath. It also ships a request-flow engine for building and running multi-step API sequences directly inside the UI.

DocTreen UI DocTreen UI


Contents


Installation

npm install doctreen

For NestJS projects, also install the peer dependencies (if not already present):

npm install reflect-metadata rxjs

For Zod schema support:

npm install zod

Quick Start

Express

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'));

Fastify

const fastify = require('fastify')();
const { fastifyAdapter } = require('doctreen/fastify');

// Call BEFORE registering routes — uses the onRoute hook
fastifyAdapter(fastify, {
  meta: { title: 'My API', version: '1.0.0' },
});

fastify.get('/users', async (req, reply) => reply.send([]));

fastify.listen({ port: 3000 });

Hono

// Hono v4 is ESM-only — run with: npx tsx hono-app.js
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { honoAdapter } from 'doctreen/hono';

const app = new Hono();

app.get('/users', (c) => c.json([]));

// Can be called before or after routes
honoAdapter(app, { meta: { title: 'My API', version: '1.0.0' } });

serve({ fetch: app.fetch, port: 3000 });

Koa

const Koa    = require('koa');
const Router = require('@koa/router');
const { koaAdapter } = require('doctreen/koa');

const app    = new Koa();
const router = new Router();

router.get('/users', (ctx) => { ctx.body = []; });

// Can be called before or after routes
koaAdapter(router, { meta: { title: 'My API', version: '1.0.0' } });

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);

NestJS

import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { nestAdapter } from 'doctreen/nest';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Call after NestFactory.create(), before app.listen()
  nestAdapter(app, {
    meta: { title: 'My API', version: '1.0.0' },
  });

  await app.listen(3000);
  console.log('Docs at http://localhost:3000/docs');
}
bootstrap();

Then annotate your controller methods with @DocRoute (see NestJS — Decorator API).

Visit the configured docsPath (default: /docs) to see your documentation.


Configuration

All adapters accept the same config object:

{
  docsPath:  '/docs',          // URL where the docs UI is served
  enabled:   true,             // Set false to disable; defaults to NODE_ENV !== 'production'
  liveReload: false,           // Re-discover routes on every docs hit
  meta: {
    title:       'My API',
    version:     '1.0.0',
    description: 'Full description shown in the UI header',
  },
  exclude:   ['/health', /^\/internal\/.*/],  // Paths to hide from docs
  groups: {                    // Group routes under named sections in the sidebar
    Users:    ['/users', '/users/:id'],
    Products: '/products',
  },
  flows:     [...],            // Inline flow presets (see Request Flows)
  flowsPath: './doctreen-flows',  // Directory of *.json flow files
}

Config Reference

| Option | Type | Default | Description | |---|---|---|---| | docsPath | string | '/docs' | URL where the docs UI is served | | enabled | boolean | NODE_ENV !== 'production' | Set to false to disable docs entirely | | liveReload | boolean | false | Re-discover routes on every docs request | | meta.title | string | 'API Documentation' | Title shown in the UI header | | meta.version | string | '1.0.0' | Version label | | meta.description | string | '' | Description shown below the title | | exclude | string \| RegExp \| Array | [] | Routes to hide from the docs | | groups | Record<string, string \| string[]> | {} | Group routes into named sidebar sections | | flows | FlowDefinition[] | null | Inline request-flow presets | | flowsPath | string | auto-detected | Directory of *.json flow files |


NestJS — Decorator API

DocTreen provides a decorator-based API for NestJS that integrates naturally with standard NestJS controller patterns.

Setup

// main.ts
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { nestAdapter } from 'doctreen/nest';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  nestAdapter(app, { meta: { title: 'My API', version: '2.0.0' } });
  await app.listen(3000);
}
bootstrap();

No changes to AppModule are required. DocTreen reads NestJS's internal route metadata directly — no DiscoveryModule import needed.

Works with both @nestjs/platform-express (default) and @nestjs/platform-fastify.

@DocRoute — Full schema on one decorator

import { Controller, Post, Body } from '@nestjs/common';
import { DocRoute } from 'doctreen/nest';
import { z } from 'zod';

const importSchema = z.object({
  products: z.array(z.object({ sku: z.string(), price: z.number() })),
});

const importResponseSchema = z.object({
  imported: z.number(),
  skipped:  z.number(),
});

@Controller('products')
export class ProductsController {
  @Post('import')
  @DocRoute({
    description: 'Bulk import partner inventory',
    headers: {
      'x-partner-api-key': 'Partner API key',
      'Content-Type':      'application/json',
    },
    request: {
      body: importSchema,
    },
    response: importResponseSchema,
    errors: {
      400: 'Validation failed',
      401: 'Missing or invalid API key',
      429: 'Rate limit exceeded',
    },
  })
  importProducts(@Body() body: any) {
    return { imported: body.products.length, skipped: 0 };
  }
}

Granular decorators

Use the smaller decorators when you want to keep each concern separate, or when composing with other decorator libraries:

import { Controller, Get, Post, Delete, Param, Body } from '@nestjs/common';
import {
  DocDescription,
  DocHeaders,
  DocRequest,
  DocResponse,
  DocErrors,
} from 'doctreen/nest';
import { z } from 'zod';

const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string() });

@Controller('users')
export class UsersController {
  @Get()
  @DocDescription('List all users')
  @DocRequest({ query: z.object({ page: z.number().optional(), role: z.string().optional() }) })
  @DocResponse(z.array(UserSchema))
  getUsers() { return []; }

  @Post()
  @DocDescription('Create a new user')
  @DocHeaders({ Authorization: 'Bearer <token>' })
  @DocRequest({ body: z.object({ name: z.string(), email: z.string() }) })
  @DocResponse(UserSchema)
  @DocErrors({ 409: 'Email already in use', 422: 'Validation failed' })
  createUser(@Body() body: any) { return { id: 1, ...body }; }

  @Delete(':id')
  @DocDescription('Delete a user by ID')
  @DocErrors({ 404: 'User not found', 403: 'Insufficient permissions' })
  deleteUser(@Param('id') id: string) { return { success: true }; }
}

All @Doc* decorators merge onto the same metadata key — stack any combination on the same method.

@DocRoute schema reference

| Field | Type | Description | |---|---|---| | description | string | Human-readable description shown in the UI | | headers | Record<string, string> | Request headers to document | | request.body | SchemaNode \| ZodSchema | Request body shape | | request.query | SchemaNode \| ZodSchema | Query parameter shape | | response | SchemaNode \| ZodSchema | Success response shape | | errors | Record<number, string \| { description?, schema? }> | Error responses by HTTP status |


Zod Support

DocTreen includes a standalone Zod → SchemaNode converter available as doctreen/zod. It is used automatically inside the NestJS adapter whenever a Zod schema is passed to @DocRoute or any @Doc* decorator.

zodToSchemaNode

import { z } from 'zod';
import { zodToSchemaNode } from 'doctreen/zod';

const schema = z.object({
  id:    z.number(),
  name:  z.string(),
  email: z.string().email(),
  role:  z.enum(['admin', 'user']).optional(),
  tags:  z.array(z.string()),
});

const node = zodToSchemaNode(schema);
// {
//   type: 'object',
//   properties: {
//     id:    { type: 'number' },
//     name:  { type: 'string' },
//     email: { type: 'string' },
//     role:  { type: 'string', optional: true },
//     tags:  { type: 'array', items: { type: 'string' } },
//   }
// }

Supported Zod types

| Zod type | SchemaNode output | |---|---| | z.string(), z.date() | { type: 'string' } | | z.number(), z.bigint() | { type: 'number' } | | z.boolean() | { type: 'boolean' } | | z.null(), z.undefined(), z.void() | { type: 'null' } | | z.any(), z.unknown() | { type: 'unknown' } | | z.object({...}) | { type: 'object', properties: {...} } | | z.array(T) | { type: 'array', items: T } | | z.tuple([T, ...]) | { type: 'array', items: T[0] } | | z.record(V) | { type: 'object', properties: {} } | | z.optional(T) | { ...T, optional: true } | | z.nullable(T) | { ...T, optional: true } | | z.default(T) | unwraps to T | | z.enum([...]) | { type: 'string' } | | z.nativeEnum(E) | { type: 'string' } or { type: 'number' } | | z.literal(v) | { type: typeof v } | | z.union([A, B]) | first option | | z.discriminatedUnion(...) | first option | | z.intersection(A, B) | merged object properties | | z.lazy(...) | resolved recursively | | .transform(), .pipe() | unwraps to input schema |

Using zodToSchemaNode with other adapters

While the NestJS adapter handles Zod conversion automatically, you can use zodToSchemaNode with any adapter via defineRoute:

const { defineRoute } = require('doctreen/express');
const { zodToSchemaNode } = require('doctreen/zod');
const { z } = require('zod');

const CreateUserSchema = z.object({ name: z.string(), email: z.string() });

app.post('/users', defineRoute(
  (req, res) => res.status(201).json({ id: 1 }),
  {
    description: 'Create a user',
    request:  { body: zodToSchemaNode(CreateUserSchema) },
    response: zodToSchemaNode(CreateUserSchema.extend({ id: z.number() })),
  }
));

Request Flows

DocTreen loads named request-flow presets into the docs UI and runs them through a shared engine — also available as a CLI.

Flows are first-class in the UI:

  • A top-level Flows tab keeps flows separate from route docs
  • The Flows tab includes a built-in guide for writing flow JSON
  • The built-in flow creator lets you assemble draft steps from documented routes
  • Runtime inputs and per-run baseUrl overrides are supported
  • {{input.*}}, {{vars.*}}, and {{env.*}} placeholders are supported in all step fields
  • Prior response fields can be promoted into extract entries and reused as {{vars.*}}
  • Results are shown as both a visual execution timeline and raw JSON

Directory-based loading

Place *.json files in a ./doctreen-flows directory and they will be loaded automatically. Use flowsPath for a custom location:

app.use(expressAdapter(app, {
  flowsPath: path.join(__dirname, 'doctreen-flows'),
  meta: { title: 'My API', version: '1.0.0' },
}));

Inline flows

app.use(expressAdapter(app, {
  flows: [
    {
      version: 1,
      name: 'Login smoke test',
      baseUrl: 'http://localhost:3000',
      steps: [
        {
          id: 'login',
          request: {
            method: 'POST',
            path: '/auth/login',
            body: { email: '[email protected]', password: 'secret' },
          },
          assert: { status: 200 },
          extract: { token: { from: 'body', path: '$.token' } },
        },
      ],
    },
  ],
}));

Flow format

{
  "version": 1,
  "name": "User onboarding",
  "description": "Create a user, fetch it back, then delete it.",
  "baseUrl": "http://localhost:3000",
  "inputs": {
    "email": { "type": "string", "required": true },
    "name":  { "type": "string", "required": true }
  },
  "steps": [
    {
      "id": "create-user",
      "request": {
        "method": "POST",
        "path": "/users",
        "body": { "email": "{{input.email}}", "name": "{{input.name}}" }
      },
      "extract": {
        "userId": { "from": "body", "path": "$.id" }
      },
      "assert": {
        "status": 201,
        "body": { "$.email": "{{input.email}}" }
      }
    },
    {
      "id": "get-user",
      "request": { "method": "GET", "path": "/users/{{vars.userId}}" },
      "assert": {
        "status": 200,
        "exists": ["$.id", "$.createdAt"]
      }
    }
  ]
}

Variable namespaces

| Namespace | Source | |---|---| | {{input.*}} | Runtime values entered in the docs UI or CLI | | {{vars.*}} | Values extracted from previous step responses | | {{env.*}} | Values from the flow file or environment overrides |

CLI runner

doctreen-flow run doctreen-flows/user-onboarding.json \
  --input [email protected] \
  --input name='Alice Smith'

# Named environment
doctreen-flow run doctreen-flows/user-onboarding.json --env staging

# Explicit environment file + JSON report
doctreen-flow run doctreen-flows/user-onboarding.json \
  --env ./doctreen-flows/environments/staging.json \
  --report json

CLI flags:

| Flag | Description | |---|---| | --env <name\|file> | Load an environment preset | | --base-url <url> | Override baseUrl for this run | | --input key=value | Supply runtime inputs | | --no-bail | Continue running after a failed step | | --report text\|json | Output format (default: text) |


Documenting Routes with JSDoc

JSDoc parsing is available for Express, Fastify, Hono, and Koa.
For NestJS, use @DocRoute and the decorator API instead.

DocTreen reads JSDoc from handler function source at runtime. Place the JSDoc block inside the handler function body (at the top):

app.get('/users/:id', function (req, res) {
  /**
   * @description Get a user by ID
   * @param  {string} query.fields  Comma-separated fields to return
   * @response {string} id
   * @response {string} name
   * @response {string} email
   */
  res.json({ id: req.params.id, name: 'Alice', email: '[email protected]' });
});

Supported JSDoc tags

| Tag | Description | Example | |---|---|---| | @description | Route description | @description Get all users | | @param {type} body.name | Request body field | @param {string} body.email | | @param {type} query.name | Query parameter | @param {string} query.search | | @response {type} name | Response field | @response {string} id | | @returns {Type} | Full response type (schema ref) | @returns {User} | | @header Name - value | Request header | @header Authorization - Bearer <token> |

Wrap a field name in [brackets] to mark it optional:

/**
 * @param {string}  body.name
 * @param {string}  [body.bio]    optional
 * @param {number}  [body.age]    optional
 */

Schema Builder

DocTreen ships a lightweight schema builder (s) for defining typed request and response shapes without a runtime schema library.

const { s } = require('doctreen');
// import { s } from 'doctreen';

s.string()            // { type: 'string' }
s.number()            // { type: 'number' }
s.boolean()           // { type: 'boolean' }
s.null()              // { type: 'null' }
s.unknown()           // { type: 'unknown' }

s.object({
  id:   s.number(),
  name: s.string(),
  bio:  s.optional(s.string()),   // optional field — shown with ? in the UI
})

s.array(s.string())
s.array(s.object({ id: s.number(), tag: s.string() }))

s.optional(s.string())  // wraps any node as optional

s is re-exported from all adapter packages for convenience:

const { s } = require('doctreen/express');
const { s } = require('doctreen/fastify');
const { s } = require('doctreen/hono');
const { s } = require('doctreen/koa');
const { s } = require('doctreen/nest');

Named Schemas

Register a schema under a name and reference it in JSDoc with {TypeName} or {TypeName[]}:

const { defineSchema, s } = require('doctreen');

defineSchema('User', s.object({
  id:     s.number(),
  name:   s.string(),
  email:  s.string(),
  active: s.boolean(),
  bio:    s.optional(s.string()),
}));

app.get('/users', function (req, res) {
  /**
   * @description List all users
   * @returns {User[]}
   */
  res.json([]);
});

app.get('/users/:id', function (req, res) {
  /**
   * @description Get a user by ID
   * @returns {User}
   */
  res.json({ id: 1, name: 'Alice', email: '[email protected]', active: true });
});

defineSchema is also re-exported from all adapter packages.


Explicit Route Definition with defineRoute

For full control, wrap a handler with defineRoute. Works the same across Express, Fastify, Hono, and Koa:

const { defineRoute, s } = require('doctreen/express');
// const { defineRoute, s } = require('doctreen/fastify');
// import { defineRoute, s } from 'doctreen/hono';
// const { defineRoute, s } = require('doctreen/koa');

app.post('/users', defineRoute(
  (req, res) => {
    res.status(201).json({ id: 1, name: req.body.name });
  },
  {
    description: 'Create a new user account',
    headers: {
      Authorization:  'Bearer <token>',
      'Content-Type': 'application/json',
    },
    request: {
      body:  s.object({ name: s.string(), email: s.string(), role: s.optional(s.string()) }),
      query: null,
    },
    response: s.object({ id: s.number(), name: s.string(), email: s.string() }),
    errors: {
      409: 'Email address already in use',
      422: { description: 'Validation failed', schema: s.object({ message: s.string(), field: s.string() }) },
    },
  }
));

Schema resolution order (first wins):

| Adapter | Priority | |---|---| | Express | defineRoute → JSDoc → runtime inference | | Fastify | defineRoute → Fastify native JSON Schema → JSDoc | | Hono | defineRoute → JSDoc | | Koa | defineRoute → JSDoc | | NestJS | @DocRoute / @Doc* decorators |

Fastify native JSON Schema

If you already annotate routes with Fastify's built-in schema option, DocTreen reads it automatically — no defineRoute needed:

fastify.get('/users/:id', {
  schema: {
    description: 'Get a user by ID',
    response: {
      200: {
        type: 'object',
        properties: {
          id:    { type: 'number' },
          name:  { type: 'string' },
          email: { type: 'string' },
        },
      },
    },
  },
  handler: async (req, reply) => { /* ... */ },
});

Error Responses

Document possible error responses via the errors field (available in defineRoute and @DocRoute):

errors: {
  // Plain string — description only
  401: 'Missing or invalid Authorization header',

  // Object — description + error body schema
  422: {
    description: 'Validation failed',
    schema: s.object({ message: s.string(), field: s.string() }),
  },

  // Object — schema only
  500: { schema: s.object({ message: s.string() }) },
}

Error responses appear in each route's detail panel, colour-coded by class (amber for 4xx, red for 5xx), and are exported as saved examples when downloading a Postman collection.


UI Features

Route Browser

  • Sidebar groups routes by section (from groups config) or by first path segment
  • Color-coded method pills — GET, POST, PUT, PATCH, DELETE
  • Property-count badge on routes with defined schemas
  • Lives in its own Routes tab when flows are also enabled

Route Detail Panel

  • Full schema tree for request body, query params, path params, and response
  • Error responses — colour-coded by status class, with optional body schema
  • Optional fields shown with ? suffix
  • Copy as cURL — ready-to-run curl command with example values filled in
  • Copy for LLM — structured markdown description of the endpoint (useful for AI-assisted development)
  • Export to Postman — downloads a Postman Collection v2.1 JSON; error responses are included as saved example responses

Flow Runner

  • Dedicated Flows tab, separate from route documentation
  • Built-in authoring guide with JSON examples
  • Flow creator — assemble steps from documented routes, insert placeholders, map path params
  • Capture prior response fields and convert them to extract rules automatically
  • Run flows inside the UI with live input collection and baseUrl override
  • Visual execution timeline with per-step request/response details
  • Raw JSON output from the shared runner
  • Same format works with the doctreen-flow CLI

TypeScript

DocTreen ships declaration files alongside the JavaScript. No separate @types/doctreen package is needed.

Express

import express from 'express';
import { expressAdapter, defineRoute, RouteSchemas } from 'doctreen/express';
import { s } from 'doctreen';

const app = express();
app.use(express.json());

app.post('/users', defineRoute<{ name: string }, never, { id: number; name: string }>(
  (req, res) => res.status(201).json({ id: 1, name: req.body.name }),
  {
    description: 'Create a user',
    request:  { body: s.object({ name: s.string() }) },
    response: s.object({ id: s.number(), name: s.string() }),
    errors:   { 409: 'Email already in use' },
  }
));

app.use(expressAdapter(app, { meta: { title: 'My API', version: '1.0.0' } }));
app.listen(3000);

Fastify

import Fastify from 'fastify';
import { fastifyAdapter } from 'doctreen/fastify';

const fastify = Fastify();
fastifyAdapter(fastify, { meta: { title: 'My API', version: '1.0.0' } });

fastify.get('/users', async (req, reply) => reply.send([]));
fastify.listen({ port: 3000 });

Hono

import { Hono } from 'hono';
import { honoAdapter } from 'doctreen/hono';

const app = new Hono();
honoAdapter(app, { meta: { title: 'My API', version: '1.0.0' } });

Koa

import Koa from 'koa';
import Router from '@koa/router';
import { koaAdapter } from 'doctreen/koa';

const app    = new Koa();
const router = new Router();

koaAdapter(router, { meta: { title: 'My API', version: '1.0.0' } });

app.use(router.routes());
app.use(router.allowedMethods());

NestJS

import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { Controller, Get, Post, Body, Module } from '@nestjs/common';
import { nestAdapter, DocRoute, DocDescription, DocResponse } from 'doctreen/nest';
import { z } from 'zod';

const UserSchema = z.object({ id: z.number(), name: z.string() });

@Controller('users')
class UsersController {
  @Get()
  @DocDescription('List all users')
  @DocResponse(z.array(UserSchema))
  getUsers() { return []; }

  @Post()
  @DocRoute({
    description: 'Create a user',
    request:  { body: z.object({ name: z.string() }) },
    response: UserSchema,
    errors:   { 409: 'Email already in use' },
  })
  createUser(@Body() body: { name: string }) {
    return { id: 1, ...body };
  }
}

@Module({ controllers: [UsersController] })
class AppModule {}

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  nestAdapter(app, { meta: { title: 'My API', version: '1.0.0' } });
  await app.listen(3000);
}
bootstrap();

Available types

import type {
  SchemaNode,       // { type, properties?, items?, optional? }
  RouteEntry,       // { method, path, params, description, errors, ... }
  ErrorEntry,       // { status, description, schema }
  UserConfig,       // full config object shape
  NormalizedConfig,
  ApiMeta,
  RouteRegistry,
} from 'doctreen';

import type { RouteSchemas, ExpressLike }              from 'doctreen/express';
import type { RouteSchemas, FastifyLike }              from 'doctreen/fastify';
import type { RouteSchemas, HonoLike }                 from 'doctreen/hono';
import type { RouteSchemas, KoaRouterLike }            from 'doctreen/koa';
import type { NestRouteSchemas, NestApplicationLike }  from 'doctreen/nest';

The structural interfaces (ExpressLike, FastifyLike, etc.) let you use doctreen without depending on a specific framework's type package.


How It Works

Express

  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 real HTTP traffic automatically populates request and response schemas.
  4. JSDoc comments inside handler functions are parsed via fn.toString() at runtime.
  5. If flows are configured, POST /docs/__flows/run executes them through the shared runner.

Fastify

  1. fastifyAdapter(fastify, config) registers an onRoute hook and adds the docs GET route.
  2. Every route added after fastifyAdapter is captured by the hook at registration time — no traffic needed.
  3. Schema resolution order: defineRoute → Fastify native JSON Schema → JSDoc.

Hono

  1. honoAdapter(app, config) adds a GET route at docsPath to the Hono app.
  2. On the first request to the docs page, app.routes is read — all routes registered by that time are shown.
  3. Can be called before or after your routes (lazy read at request time).
  4. Schema resolution order: defineRoute → JSDoc.

Koa

  1. koaAdapter(router, config) adds a GET route at docsPath to the @koa/router instance.
  2. On the first request to the docs page, router.stack is read — all routes registered by that time are shown.
  3. Can be called before or after your routes (lazy read at request time).
  4. Schema resolution order: defineRoute → JSDoc.

NestJS

  1. nestAdapter(app, config) is called after NestFactory.create() and before app.listen().
  2. It registers the docs route directly on the underlying HTTP adapter (Express or Fastify platform), bypassing NestJS guards and interceptors — this is intentional for an internal docs endpoint.
  3. Route discovery reads NestJS's internal container (app.container.getModules()) to enumerate all controllers and their methods.
  4. For each controller method, it reads Reflect.getMetadata('path', fn) and Reflect.getMetadata('method', fn) — the same metadata keys set by @Get(), @Post(), etc.
  5. @DocRoute and @Doc* decorators attach their schemas under a separate metadata key on the same method function.
  6. The global prefix (set via app.setGlobalPrefix(...)) is prepended to all discovered paths.
  7. Schema resolution order: @DocRoute / @Doc* decorators (no JSDoc or runtime-inference fallback in NestJS).

Example Apps

npm run example             # Express JS     → http://localhost:3000/api/docs
npm run example:ts          # Express TS     → http://localhost:3000/api/docs
npm run example:fastify     # Fastify JS     → http://localhost:3001/api/docs
npm run example:fastify:ts  # Fastify TS     → http://localhost:3001/api/docs
npm run example:hono        # Hono JS        → http://localhost:3002/api/docs
npm run example:hono:ts     # Hono TS        → http://localhost:3002/api/docs
npm run example:koa         # Koa JS         → http://localhost:3003/api/docs
npm run example:koa:ts      # Koa TS         → http://localhost:3003/api/docs
npm run example:nest        # NestJS TS      → http://localhost:3001/docs

| File | Framework | Highlights | |---|---|---| | example/app.js | Express | JSDoc, defineRoute, named schemas, error responses, flow presets | | example/app.ts | Express | Fully typed with defineRoute generics, flow presets | | example/fastify-app.js | Fastify | JSDoc, defineRoute, Fastify native JSON Schema, flow presets | | example/fastify-app.ts | Fastify | Fully typed with Fastify route generics, flow presets | | example/hono-app.js | Hono | JSDoc, defineRoute; run via npx tsx | | example/hono-app.ts | Hono | Fully typed with Hono Context | | example/koa-app.js | Koa | JSDoc, defineRoute, @koa/router | | example/koa-app.ts | Koa | Fully typed with Router.RouterContext | | example/nest-app.ts | NestJS | @DocRoute, @Doc* decorators, Zod schemas, s builder |


License

MIT