v1.1.9 live on npm

Instant API docs
for Express.

Like FastAPI's /docs, but for Node.js.
Zero annotations. Zero config. One line of code.

nodox automatically discovers your routes, reads Zod · Joi · yup · express-validator schemas without any changes to your code, and serves an interactive playground, all from a single middleware.

npm install nodox-cli
localhost:3000/__nodox
6 routes  ·  4 schemas
GET POST PUT DEL
GET /api/users confirmed
POST /api/users confirmed
GET /api/users/:id inferred
PUT /api/users/:id observed
DELETE /api/users/:id
GET /api/health observed
POST /api/users
Body
{ "name": "Alice Chen", "email": "alice@example.com", "age": 28 }
Headers
Content-Type : application/json
Response 200 43ms
{ "id": 7, "name": "Alice Chen", "email": "alice@example.com", "createdAt": "2026-04-05T10:23:00Z" }
Routes
POST /api/users
GET /api/users/:id
GET /api/orders +
DELETE /api/users/:id +
POST
/api/users
GET
/api/users/:id

Every other approach makes you do the work.

API documentation has always been a second job. Annotations, decorators, YAML specs, manual schema registration; the tool expects you to write what the code already knows.

The old way
You write docs alongside code

JSDoc comments, decorators, or OpenAPI YAML; documentation lives in a separate layer that drifts the moment you rename a field or add a param.

First request required

Tools that learn from live traffic can't show schemas for new routes until someone hits them. Fresh routes are invisible until real traffic arrives.

Your validator is ignored

You already defined every field in Zod or Joi for runtime validation. Most tools don't read it, you define the same type twice, in two different formats.

Endpoints in isolation

Real API usage is sequential: create a resource, fetch it, act on it. Documentation tools show endpoints one at a time with no way to model dependencies.

nodox
Zero annotation

One middleware line. Routes appear the moment they're registered. There is nothing else to write; the code is the documentation.

Documented before first request

Schemas are read at registration time. Your test suite teaches it the rest. New routes are fully documented before a single user ever reaches them.

Reads your validators natively

Zod, Joi, yup, express-validator, detected automatically at the framework level. One schema, zero duplication, always in sync.

Visual chain builder

Connect endpoints, map data between calls, and simulate entire multi-step API flows in the browser, with no extra tooling or setup.

nodox is the first Express middleware to
01 Auto-detect Zod, Joi, yup, and express-validator schemas at route-registration time, before any request, with no wrapper functions or manual registration required.
02 Learn your complete API surface from your existing test suite automatically, with zero changes to any test file.
03 Offer a visual chain builder for modeling and simulating multi-step API flows, connecting output fields to input params across dependent calls.
04 Combine schema diffing, a live request playground, real-time WebSocket updates, and zero-config setup in a single app.use() call.

Everything your API docs need.
Nothing you don't.

Zero Config

Add one middleware. Routes appear instantly. No YAML, no JSDoc, no decorators, no code generators.

Auto Schema Detection

Reads Zod, Joi, yup, and express-validator schemas automatically. No wrapper functions required for detection.

Interactive Playground

Test every route directly from the browser. Path params, query strings, headers, and request body editors built-in.

Real-time Updates

WebSocket-powered live sync. The UI updates as routes are registered and schemas are learned, no refresh needed.

Test Suite Seeding

Run npx nodox-cli init once. Your existing test suite automatically teaches nodox every request and response shape.

TypeScript Ready

Ships with full type definitions. No @types/ package needed. Works with Zod's inferred types out of the box.

Up in under two minutes.

1 Install
bash
npm install nodox-cli
2 Mount
javascript
import express from 'express'
import nodox, { validate } from 'nodox-cli'
import { z } from 'zod'

const app = express()
app.use(express.json())
app.use(nodox(app))          // ← one line

const CreateUser = z.object({
  name:  z.string().min(1),
  email: z.string().email(),
  age:   z.number().int().min(0).optional(),
})

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

// validate() handles validation AND registers the schema
app.post('/users',
  validate(CreateUser, { response: UserResponse }),
  (req, res) => {
    res.status(201).json({ id: 1, ...req.body })
  }
)

app.get('/users/:id', (req, res) => {
  res.json({ id: Number(req.params.id), name: 'Alice', email: 'alice@example.com' })
})

app.listen(3000)
// → http://localhost:3000/__nodox
javascript
import express from 'express'
import nodox, { validate } from 'nodox-cli'
import Joi from 'joi'

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

const CreateUser = Joi.object({
  name:  Joi.string().min(1).required(),
  email: Joi.string().email().required(),
  age:   Joi.number().integer().min(0),
})

app.post('/users', validate(CreateUser), (req, res) => {
  res.status(201).json({ id: 1, ...req.body })
})

app.get('/users', (req, res) => {
  res.json([{ id: 1, name: 'Alice' }])
})

app.listen(3000)
// → http://localhost:3000/__nodox
javascript
import express from 'express'
import nodox from 'nodox-cli'
import { body, validationResult } from 'express-validator'

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

// nodox reads ValidationChain metadata automatically —
// no validate() wrapper needed for express-validator
app.post('/users',
  body('email').isEmail(),
  body('name').isString().notEmpty(),
  body('age').isInt({ min: 0 }).optional(),
  (req, res) => {
    const errors = validationResult(req)
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() })
    }
    res.status(201).json({ id: 1, ...req.body })
  }
)

app.listen(3000)
// → http://localhost:3000/__nodox
// Field types are detected: email → string/email, age → integer
javascript
import express from 'express'
import nodox from 'nodox-cli'

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

// No validation? No problem.
// nodox learns schemas from live traffic automatically.
app.get('/users', (req, res) => {
  res.json([{ id: 1, name: 'Alice', role: 'admin' }])
})

app.post('/users', (req, res) => {
  // Schema learned from the first real POST request
  res.status(201).json({ id: 2, ...req.body })
})

app.listen(3000)
// → http://localhost:3000/__nodox
// Schemas populate as real requests arrive (observed confidence)
3 Explore

Open http://localhost:PORT/__nodox in your browser. Routes, schemas, and the playground are ready immediately.

Optionally, run npx nodox-cli init once to wire your test suite as a schema source, no test changes needed.

Installation

bash
npm install nodox-cli
# or
yarn add nodox-cli
# or
pnpm add nodox-cli
Requirements: Node.js ≥ 18 · Express 4.x or 5.x

Basic Setup

Mount nodox before your route registrations and after your body parser. Passing the app reference enables real-time route interception.

javascript
import express from 'express'
import nodox from 'nodox-cli'

const app = express()

app.use(express.json())    // 1. Body parser first
app.use(nodox(app))        // 2. nodox second — pass app for real-time detection

// 3. Register your routes — all will be captured
app.get('/users', handler)
app.post('/users', handler)

app.listen(3000)
// → http://localhost:3000/__nodox
Note: If routes are registered before app.use(nodox(app)), they are still discovered via app._router.stack at startup, but real-time patching won't apply to them. Mount nodox first for best results.

Options

javascript
app.use(nodox(app, {
  uiPath:    '/__nodox',   // URL prefix for the docs UI
  log:       true,         // print startup info to console
  schema:    true,         // enable schema detection
  intercept: true,         // enable live request/response interception
  force:     false,        // allow running in production
}))
OptionTypeDefaultDescription
uiPathstring'/__nodox'URL prefix for the documentation UI and WebSocket endpoint.
logbooleantruePrint route count and UI URL to the console at startup.
schemabooleantrueEnable automatic schema detection (Zod/Joi/yup/express-validator).
interceptbooleantrueWrap res.json() to observe request/response shapes from live traffic.
forcebooleanfalseAllow nodox to run when NODE_ENV=production. See Production.

validate()

The recommended way to document and validate a route. It registers the schema at route-registration time (not at request time) and returns an Express middleware that validates req.body.

javascript
import { validate } from 'nodox-cli'

// Basic usage
app.post('/users', validate(schema), handler)

// With response schema (docs only — not validated at runtime)
app.post('/users', validate(schema, { response: ResponseSchema }), handler)

// Strict mode — rejects unknown fields
app.post('/users', validate(schema, { strict: true }), handler)

Supported libraries

LibraryInput validationResponse docs
Zod v3 + v4 safeParse
Joi validate
yup validateSync
Plain JSON Schema~ Display only

Validation error format

On failure, validate() returns 400 with:

json
{
  "error": "Validation failed",
  "details": [
    {
      "path": "email",
      "message": "Invalid email address",
      "code": "invalid_string"
    }
  ]
}

Signature

typescript
function validate(
  schema: object,          // Zod | Joi | yup | JSON Schema
  options?: {
    strict?: boolean       // reject unknown fields (default: false)
    response?: object      // response schema for UI documentation
  }
): RequestHandler

express-validator

nodox detects express-validator chains directly from their internal metadata at route-registration time. No validate() wrapper needed.

javascript
import { body, param } from 'express-validator'

// nodox reads these chains automatically
app.post('/users',
  body('email').isEmail(),          // → string, format: email
  body('name').isString(),          // → string
  body('age').isInt({ min: 0 }),    // → integer
  body('active').isBoolean(),       // → boolean
  handler
)

Inferred type mapping

ValidatorJSON Schema type
isEmail()string, format: email
isURL()string, format: uri
isUUID()string, format: uuid
isISO8601()string, format: date-time
isInt() / isNumeric()integer
isFloat() / isDecimal()number
isBoolean()boolean
isArray()array
isJSON()object
anything elsestring
Supports express-validator v6 (._context) and v7 (.builder). Confidence level: inferred.

Schema Detection Layers

nodox uses a priority chain. Higher confidence always wins; a schema is never downgraded.

LayerConfidenceTriggerBundler-safe?
validate()confirmedRoute registration Yes
express-validatorinferredRoute registration Yes
Dry-runinferredStartup tick~ Plain source only
Test cacheobservedServer startup Yes
Live interceptionobservedReal traffic Yes

Test Suite Seeding

nodox can learn every request and response shape from your existing test suite, no test changes needed.

bash
npx nodox-cli init    # detects Jest or Vitest, injects setup file

This adds nodox-cli/jest-setup to your setupFiles. The setup file patches http.request during tests to record every local HTTP exchange and writes shapes to .apicache.json on process exit.

On next server startup, schemas from .apicache.json are pre-loaded, including parameterized routes like /users/:id matched from test calls to /users/123.

.apicache.json is add-only (never removes fields). Run npx nodox-cli prune after significant API changes to wipe and start fresh.

CLI Reference

CommandDescription
npx nodox-cli initDetect Jest/Vitest and inject nodox-cli/jest-setup into the config. Adds .apicache.json to .gitignore.
npx nodox-cli statusShow cache stats: route count, schemas with input/output, last updated.
npx nodox-cli pruneWipe .apicache.json and start fresh. Use after major API changes.

TypeScript

nodox ships with complete type definitions. No additional @types/ package needed.

typescript
import nodox, { validate, NodoxOptions, ValidateOptions } from 'nodox-cli'
import type { RequestHandler } from 'express'
import { z } from 'zod'

const opts: NodoxOptions = {
  uiPath: '/api-docs',
  force: false,
}

app.use(nodox(app, opts))

const Schema = z.object({ name: z.string() })

const handler: RequestHandler = (req, res) => {
  res.json({ ok: true })
}

app.post('/items', validate(Schema), handler)
Ensure your tsconfig.json uses "moduleResolution": "bundler", "node16", or "nodenext" to resolve the exports field in package.json.

Production Safety

nodox is disabled by default when NODE_ENV=production. It returns a no-op middleware and prints a warning.

text
[nodox] Disabled in production (NODE_ENV=production).
        Pass { force: true } to override — but do not expose /__nodox publicly.

To intentionally enable nodox in a non-development environment (for example, behind an internal auth gateway):

javascript
app.use(nodox(app, { force: true }))
Never expose /__nodox publicly without authentication. It reveals your full API surface including route structure, parameter names, and schema shapes, and provides a working request playground.

Compatibility

DependencyVersionStatus
Node.js≥ 18.0.0 Required
Express4.x · 5.x Fully supported
Zodv3 · v4 Auto-detected
Joi≥ 17 Auto-detected
yup≥ 0.32 Auto-detected
express-validatorv6 · v7 Auto-detected
Jest≥ 27 npx nodox-cli init
Vitest≥ 0.28 npx nodox-cli init

Common issues & solutions.

Everything developers run into, and how to fix it.

Schema not showing in the UI

Cause: The route uses inline validation without validate(), and no requests have been made yet for live interception to observe.

Fix: Wrap your validator with validate(schema) for immediate, confirmed schemas. Or make a test request to the route; live interception will observe and populate it.

// Before (no schema in UI until traffic arrives)
app.post('/users', (req, res) => {
  const result = schema.safeParse(req.body)
  ...
})

// After (schema appears immediately at startup)
app.post('/users', validate(schema), handler)

Playground shows "No body parser detected"

Cause: express.json() is not mounted, or is mounted after nodox.

Fix: Add app.use(express.json()) before app.use(nodox(app)).

app.use(express.json())   // ← first
app.use(nodox(app))       // ← second

Routes from sub-routers not appearing

Cause: app.use('/api', router) was called before app.use(nodox(app)). The app-patcher couldn't intercept the mount.

Fix: Mount nodox as early as possible, before any app.use() calls that register routers.

app.use(express.json())
app.use(nodox(app))          // ← mount before routers
app.use('/api/v1', v1Router)
app.use('/api/v2', v2Router)

nodox is disabled: no-op in production

Cause: NODE_ENV=production is set. nodox disables itself to prevent accidental exposure.

Fix: This is intentional. If you need nodox on staging (behind auth), pass { force: true }. Otherwise set NODE_ENV=development locally.

// Staging only — ensure auth is in front
app.use(nodox(app, { force: true }))

WebSocket not connecting (WS error in browser)

Cause: A reverse proxy (nginx, Caddy, HAProxy) is not forwarding WebSocket upgrade requests.

Fix: Configure your proxy to pass through WebSocket connections on the /__nodox_ws path.

# nginx example
location /__nodox_ws {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

npx nodox-cli init says "No Jest or Vitest config found"

Cause: The command was run from a subdirectory, or the project uses an unusual config file name.

Fix: Run it from your project root (where package.json lives). Supported: jest.config.js/ts/json, vitest.config.js/ts, or a "jest" key in package.json.

TypeScript can't find nodox types

Cause: moduleResolution is set to "node" (legacy), which doesn't read the exports field in package.json.

Fix: Update tsconfig.json:

{
  "compilerOptions": {
    "moduleResolution": "bundler"
  }
}

Dry-run causes unexpected behavior at startup

Cause: nodox calls middleware with a mock request to detect schemas. If a function has side effects (rate-limiter increments, audit logs, DB writes), they fire once at startup.

Fix: Use validate(schema) on affected routes; it registers at route-creation time, completely skipping the dry-run.

Schema not detected after switching to a bundler

Cause: The dry-run source screener inspects fn.toString() for validator patterns. After bundling/minification with esbuild, SWC, or Babel, function source is unrecognizable.

Fix: Use validate(); it registers at function creation time, not source inspection time.

.apicache.json not updating after test runs

Cause: The setup file was not injected, or tests use a test server that doesn't make real HTTP requests to localhost.

Fix: Run npx nodox-cli status to check the cache. If empty, re-run npx nodox-cli init and ensure your tests use supertest or real http.request calls.

Schema shows wrong structure after API changes

Cause: .apicache.json is add-only; old fields are never removed. Stale data from prior test runs persists.

Fix: Run npx nodox-cli prune to wipe the cache, then re-run your test suite.

npx nodox-cli prune  # wipe stale cache
npm test         # rebuild from tests

nodox() called multiple times: duplicate routes

Cause: app.use(nodox()) called more than once, or imported in both a route file and the main server file.

Fix: Call nodox() exactly once in your main server entry point. It's a singleton per app instance.

Read before you ship.

Important caveats to understand before using nodox in any environment.

Never expose /__nodox publicly without authentication

The nodox UI reveals your complete API surface: every route path, parameter name, schema shape, and a fully functional request playground. An attacker with access can enumerate your entire API and send arbitrary requests through the playground.

nodox is disabled by default in production (NODE_ENV=production). If you enable it with force: true, ensure it is protected by authentication middleware.

The playground sends real requests with real effects

Every request you send from the nodox playground hits your actual server, executes your actual route handlers, and can write to your actual database or trigger side effects. It is not a mock environment.

Use test data. Be aware of what each endpoint does before sending requests from the playground in any non-isolated environment.

Dry-run fires actual middleware code at startup

When nodox detects a possible validator via source screening, it calls that middleware with a mock request during the startup tick. If the function has side effects: rate-limiter increments, audit log entries, external API calls, database writes, those will execute once at server startup.

Use validate(schema) for routes whose handlers have side effects. This registers the schema directly and completely skips the dry-run.

Source screening breaks under bundlers and minifiers

The dry-run layer (Layer 3) works by inspecting fn.toString() for Zod/Joi patterns. After bundling with esbuild, SWC, Babel, or webpack, function source is unrecognizable, source screening produces no results.

If your dev server runs through a bundler, rely on validate() (Layer 1), express-validator detection (Layer 2), or test seeding (Layer 4) for schema coverage.

.apicache.json accumulates over time

The test suite cache is add-only; it merges new fields but never removes old ones. After significant API changes (renamed fields, removed routes), the cache will contain stale data that appears in the UI.

Run npx nodox-cli prune after major API refactors to clear the cache and rebuild it from a fresh test run.

nodox reads Express internals (app._router)

Route extraction depends on app._router.stack, which is an undocumented internal of Express. This is stable across Express 4 and 5, but could change in a future major version.

nodox checks the Express version at startup and warns if it's outside the tested range (4.x–5.x).

WebSocket uses ws:// not wss:// by default

The real-time update connection between the nodox UI and your server uses an unencrypted WebSocket (ws://). This is fine for local development (localhost). For remote access through a proxy, configure the proxy to terminate TLS and forward the connection.

The WebSocket is served at /__nodox_ws and only accepts connections from the same origin (same host) to prevent cross-site reads.

File paths are shown relative to project root

When using validate(), nodox captures the file path where the schema was defined and shows it in the UI (e.g., src/routes/users.js:42). This is relative to your project root, not absolute, no server filesystem paths are exposed to the browser.

Schema confidence never downgrades

Once a schema is registered at a given confidence level, it cannot be replaced by a lower-confidence source. A confirmed schema (from validate()) will never be overwritten by observed data from live traffic or the test cache.

This is intentional; it prevents noisy real-world traffic from corrupting your documented schema. Use validate() to lock in the authoritative schema.

Frequently asked questions.

No. Schema detection runs in a deferred setTimeout tick after startup and never blocks request handling. The response interceptor (res.json() wrapper) adds a structurally-constant overhead of shape inference, microseconds per request, negligible in practice. The WebSocket connection only carries metadata about routes and schemas, not request payloads.

Yes. nodox ships with index.d.ts and jest-setup.d.ts. All public types are exported: NodoxOptions, ValidateOptions, and the validate() function. Ensure moduleResolution is set to "bundler" or "node16" in your tsconfig.json.

confirmed: you explicitly registered the schema via validate(). This is 100% authoritative and never overwritten.

inferred: nodox detected the schema automatically (via express-validator chain metadata or dry-running a validator). Reliable but indirect.

observed: nodox learned the schema by watching real requests and responses (live interception or test seeding). Structural inference only, might miss optional fields that weren't present in observed traffic.

Not directly. nodox is disabled by default when NODE_ENV=production. If you want it accessible in a non-development environment, for example, on a private internal staging server, set { force: true } and ensure the /__nodox path is protected by authentication middleware before it reaches nodox.

We strongly recommend keeping it development-only.

No. nodox is designed specifically for Express and relies on Express internals (app._router.stack) for route extraction and the Express middleware signature for interception. Support for other frameworks is not planned at this time.

swagger-jsdoc requires you to write OpenAPI spec in JSDoc comments. swagger-autogen requires a separate generation step. Both produce static output that goes stale as your API changes.

nodox requires zero annotations and updates live. The trade-off: nodox doesn't export OpenAPI-compatible spec files (no .yaml output), it's a development tool, not a spec generator.

Not yet. The UI is a built React bundle served by the middleware. Custom UI themes and manual annotation support are planned for a future release. For now, the validate() response schema option is the primary way to add documentation beyond what nodox can detect automatically.

Route detection works for any route, including file upload routes. However, schema inference from req.body only works when a body parser populates it. Multer and similar middleware don't expose form fields through req.body in the same way, so field schemas for file upload routes are typically not auto-detected. Use validate() with a plain JSON Schema to document multipart routes.

The nodox UI auto-reconnects with exponential backoff after a server restart. When the connection is restored, the server sends a full state sync message and the UI completely replaces its state, no stale routes from the previous instance persist.

Yes, with one requirement: your proxy must forward WebSocket upgrade requests to the /__nodox_ws path. The static UI (/__nodox) works through any proxy that forwards standard HTTP. See the WebSocket not connecting issue for an nginx configuration example.