DocTreen
Features

Runtime validation

Reject invalid requests with a structured 422 — same Zod schema as the docs.

The same Zod schema you declared for documentation can validate every incoming request. Enable it once at the adapter level and DocTreen runs safeParseAsync against request.body, request.query, and request.params before your handler executes. Invalid requests are rejected with a structured 422 response.

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

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

app.post('/users', defineRoute(
  (req, res) => res.status(201).json({ id: 1, ...req.body }),
  {
    request: { body: z.object({ name: z.string().min(2), email: z.string().email() }) },
  }
));

// Turn validation on for every Zod-declared route on this app
app.use(expressAdapter(app, { validate: true }));

Sending { "email": "nope" } to POST /users now returns:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "error": "validation_failed",
  "issues": [
    { "path": "body.name",  "message": "Required",      "code": "invalid_type"   },
    { "path": "body.email", "message": "Invalid email", "code": "invalid_string" }
  ]
}

Properties

  • Opt-in. Default is off; you set validate: true once per adapter. Upgrading from v1.5 cannot suddenly start rejecting requests.
  • Per-route override. Pass validate: false to defineRoute (or @DocRoute) to skip validation on a specific route while keeping the docs entry. Pass validate: true to enable validation on one route when the adapter default is off.
  • Async refinements work — internally uses safeParseAsync, so .refine(async ...) and pipelines are honoured.
  • Zod only. Schemas built with the s.* helper are descriptive shapes, not parsers, so they cannot be used to validate. Mixed routes (some Zod, some s.*) work fine — only the Zod ones validate, others pass through.
  • All five adapters. Express, Fastify, Hono, Koa, and NestJS all support validate: true. Hono and Koa require the adapter to be called before routes (their middleware does not retro-apply); Express, Fastify, and NestJS work regardless of order.

If you previously hand-rolled a NestJS pipe or an Express middleware that ran zodSchema.parse(req.body) for every endpoint, this replaces it.

Path parameters v1.15

Validate path parameters with the same schema that documents them. Add a params schema to request and :id-style segments are checked before the handler runs — a mismatch returns the same structured 422 (issue paths are prefixed params.).

app.get('/orders/:id', defineRoute(
  (req, res) => res.json({ id: req.params.id }),
  {
    request: {
      params: z.object({ id: z.string().uuid() }),
      query:  z.object({ limit: z.coerce.number().default(50) }),
    },
  }
));

GET /orders/not-a-uuid now returns 422 with { "path": "params.id", "message": "Invalid uuid", … }. This retires the hand-rolled requireUuid helpers you'd otherwise repeat on every route. Path parameters are also typed in the OpenAPI export from the declared schema.

Write-back (coerce + defaults) v1.15

By default validation checks the request but leaves it untouched, so Zod coercions and .default()s never reach your handler. Opt in with validate: { writeback: true } and the parsed payload is written back onto the request:

app.use(expressAdapter(app, { validate: { writeback: true } }));

// query: z.object({ limit: z.coerce.number().default(50) })
//   GET /orders/abc          → req.query.limit === 50    (number, default applied)
//   GET /orders/abc?limit=7  → req.query.limit === 7     (number, coerced)
  • Express / Fastify / NestJS — written to req.body / req.query / req.params (DocTreen clears the Express 5 req.query getter for you).
  • Hono — overlaid on c.req.param() / c.req.query() / c.req.json(), and also exposed as c.get('doctreenValidated').
  • Koa — written to ctx.request.body and ctx.query; coerced path params are exposed on ctx.state.doctreenValidated.params (the Koa router re-derives ctx.params from the raw URL after validation).

Write-back is opt-in, so upgrading never silently changes what your handler sees.

Response assertion v1.15

Catch handlers that return a body which doesn't match the declared response schema. This is a development aid — it never coerces the response.

app.use(expressAdapter(app, { validate: { response: 'warn' } }));
// 'warn'  → logs the mismatch and sends the response unchanged
// 'throw' → surfaces a 500 in development
// 'off'   → default; no response checking

Runs synchronously against the declared Zod response schema (including the matching entry of a status-keyed response map), so a regression in a money- or stock-sensitive endpoint surfaces immediately in dev.

Standard error envelope v1.15

The 422 body is a stable contract — { error: 'validation_failed', issues: [{ path, message, code }] }. Routes that declared Zod validators document it in the OpenAPI export as a named DoctreenValidationError component, so codegen emits the type and clients stop guessing the shape of a validation error.

On this page