DocTreen

defineRoute

Explicit route definition with full schema control — defineRoute, @define_route, or ->doc().

For full control, attach metadata to a route explicitly. The declaration carries the same keys in every language — defineRoute wraps the handler on Node, @define_route decorates the view on Flask, ->doc() chains onto the route on Laravel:

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() }) },
    },
  }
));
from doctreen import s
from doctreen.adapters.flask import define_route


@app.post("/users")
@define_route(
    description="Create a new user account",
    headers={"Authorization": "Bearer <token>"},
    request={
        "body": s.object({
            "name": s.string(),
            "email": s.string(),
            "role": s.optional(s.string()),
        }),
    },
    response=s.object({"id": s.number(), "name": s.string(), "email": s.string()}),
    errors={
        409: "Email address already in use",
        422: {"description": "Validation failed"},
    },
)
def create_user():
    ...

@define_route attaches metadata and returns the function unchanged — it is not a wrapper. Pydantic models are accepted anywhere a schema is expected.

use Doctreen\Schema\S;

Route::post('/users', [UserController::class, 'store'])->doc([
    'description' => 'Create a new user account',
    'headers' => ['Authorization' => 'Bearer <token>'],
    'request' => [
        'body' => S::object([
            'name'  => S::string(),
            'email' => S::string(),
            'role'  => S::optional(S::string()),
        ]),
    ],
    'response' => S::object(['id' => S::number(), 'name' => S::string(), 'email' => S::string()]),
    'errors' => [
        409 => 'Email address already in use',
        422 => ['description' => 'Validation failed'],
    ],
]);

A bare schema is shorthand for the body: 'request' => S::object([...]).

Schema resolution order

AdapterPriority
ExpressdefineRoute → JSDoc → runtime inference
FastifydefineRoute → Fastify native JSON Schema → JSDoc
HonodefineRoute → JSDoc
KoadefineRoute → JSDoc
NestJS@DocRoute / @Doc* decorators
Flask@define_route → Flask URL converter types
Laravel->doc() macro

Options

The same option set in every language (camelCase keys in Python dicts, array keys in PHP):

OptionTypeNotes
descriptionstringHuman-readable description shown in the UI
summarystringShort title for the operation
headersRecord<string, string>Request headers — example value as the map value
request.bodyschemaRequest body (Zod / Pydantic / builder)
request.queryschemaQuery parameters
request.paramsschemaPath parameters (v1.15+)
responseschema | Record<number, schema>Success response — a single schema (200) or a status-keyed map
errorsRecord<number, string | { description?, schema? }>Documented error responses
validatebooleanPer-route override of adapter-level validate setting
tagsstring[]Override the path-segment default tag
securityArray<Record<string, string[]>>Per-route security override
hiddenbooleanHide from docs UI and OpenAPI
examplesRouteExamplesMulti-example bodies and responses (v1.11+)
callbacksRecord<string, CallbackDef>Per-operation OpenAPI 3.1 callbacks

Path parameters v1.15

request.params declares a schema for path parameters, alongside body and query. It's validated when validate is on (a structured 422 on mismatch) and typed as the path parameters in the OpenAPI export:

defineRoute(handler, {
  request: {
    params: z.object({ id: z.string().uuid() }),
    query:  z.object({ page: z.coerce.number().default(1) }),
    body:   z.object({ name: z.string() }),
  },
});

On Flask you often don't need this at all — /users/<int:user_id> documents the parameter as a number from the URL converter, with nothing to declare.

Status-keyed responses v1.15

response accepts either a single schema (documented as 200) or a status-keyed map, letting each status carry its own schema in the OpenAPI export. Works in all three languages — {201: Created, 200: Existing} in Python, [201 => $created] in PHP. The single-schema form is unchanged:

defineRoute(handler, {
  response: {
    201: z.object({ id: z.number() }),
    202: z.object({ queued: z.boolean() }),
  },
});

With TypeScript generics

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

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() }),
  }
));

See TypeScript for the full type catalogue.

On this page