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
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.
JSDoc comments, decorators, or OpenAPI YAML; documentation lives in a separate layer that drifts the moment you rename a field or add a param.
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.
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.
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.
One middleware line. Routes appear the moment they're registered. There is nothing else to write; the code is the documentation.
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.
Zod, Joi, yup, express-validator, detected automatically at the framework level. One schema, zero duplication, always in sync.
Connect endpoints, map data between calls, and simulate entire multi-step API flows in the browser, with no extra tooling or setup.
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.
npm install nodox-cli
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
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
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
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)
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
npm install nodox-cli
# or
yarn add nodox-cli
# or
pnpm add nodox-cli
Basic Setup
Mount nodox before your route registrations and after your body parser. Passing the app reference enables real-time route interception.
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
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
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
}))
| Option | Type | Default | Description |
|---|---|---|---|
uiPath | string | '/__nodox' | URL prefix for the documentation UI and WebSocket endpoint. |
log | boolean | true | Print route count and UI URL to the console at startup. |
schema | boolean | true | Enable automatic schema detection (Zod/Joi/yup/express-validator). |
intercept | boolean | true | Wrap res.json() to observe request/response shapes from live traffic. |
force | boolean | false | Allow 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.
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
| Library | Input validation | Response docs |
|---|---|---|
| Zod v3 + v4 | ✓ safeParse | ✓ |
| Joi | ✓ validate | ✓ |
| yup | ✓ validateSync | ✓ |
| Plain JSON Schema | ~ Display only | ✓ |
Validation error format
On failure, validate() returns 400 with:
{
"error": "Validation failed",
"details": [
{
"path": "email",
"message": "Invalid email address",
"code": "invalid_string"
}
]
}
Signature
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.
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
| Validator | JSON 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 else | string |
Schema Detection Layers
nodox uses a priority chain. Higher confidence always wins; a schema is never downgraded.
| Layer | Confidence | Trigger | Bundler-safe? |
|---|---|---|---|
validate() | confirmed | Route registration | ✓ Yes |
| express-validator | inferred | Route registration | ✓ Yes |
| Dry-run | inferred | Startup tick | ~ Plain source only |
| Test cache | observed | Server startup | ✓ Yes |
| Live interception | observed | Real traffic | ✓ Yes |
Test Suite Seeding
nodox can learn every request and response shape from your existing test suite, no test changes needed.
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.
npx nodox-cli prune after significant API changes to wipe and start fresh.
CLI Reference
| Command | Description |
|---|---|
npx nodox-cli init | Detect Jest/Vitest and inject nodox-cli/jest-setup into the config. Adds .apicache.json to .gitignore. |
npx nodox-cli status | Show cache stats: route count, schemas with input/output, last updated. |
npx nodox-cli prune | Wipe .apicache.json and start fresh. Use after major API changes. |
TypeScript
nodox ships with complete type definitions. No additional @types/ package needed.
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)
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.
[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):
app.use(nodox(app, { force: true }))
/__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
| Dependency | Version | Status |
|---|---|---|
| Node.js | ≥ 18.0.0 | ✓ Required |
| Express | 4.x · 5.x | ✓ Fully supported |
| Zod | v3 · v4 | ✓ Auto-detected |
| Joi | ≥ 17 | ✓ Auto-detected |
| yup | ≥ 0.32 | ✓ Auto-detected |
| express-validator | v6 · 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.