Express Adapter
Vision adapter for Express with tracing, Zod validation, auto-discovery, and services catalog.
Express Adapter
Vision adapter for Express adds zero-config tracing, request/response capture, a service catalog, and Zod-powered validation tooling.
Installation
bun add @getvision/adapter-express
# or
npm i @getvision/adapter-expressQuick Start
import express from 'express'
import { visionMiddleware, enableAutoDiscovery, zValidator } from '@getvision/adapter-express'
import { z } from 'zod'
const app = express()
app.use(express.json())
// Add Vision (dev only)
if (process.env.NODE_ENV !== 'production') {
app.use(visionMiddleware({ port: 9500 }))
}
// Routes
app.get('/users', (req, res) => res.json([{ id: 1, name: 'Alice' }]))
// Zod validation → schema appears in Service Catalog and API Explorer template
const CreateUser = z.object({
name: z.string().min(1).describe('Full name'),
email: z.string().email().describe('Email'),
age: z.number().int().positive().optional().describe('Age (optional)'),
})
app.post('/users', zValidator('body', CreateUser), (req, res) => res.status(201).json(req.body))
// Auto-discover routes (dev only)
if (process.env.NODE_ENV !== 'production') {
enableAutoDiscovery(app)
}
app.listen(3000)Open Dashboard at http://localhost:9500.
Features
- Automatic request tracing (root
http.requestspan + child spans) - Request/response capture (headers, body, params)
- Service Catalog with auto-grouping and manual grouping
- Zod validation with schema templates for API Explorer
- CORS headers for Dashboard (including
X-Vision-Trace-Id,X-Vision-Session)
Middleware Options
interface VisionExpressOptions {
port?: number // Dashboard port (default: 9500)
enabled?: boolean // Enable Vision (default: true)
maxTraces?: number // Trace store size (default: 1000)
maxLogs?: number // Log store size (default: 10000)
logging?: boolean // Console request logs (default: true)
cors?: boolean // CORS for Dashboard (default: true)
service?: {
name?: string
version?: string
description?: string
integrations?: Record<string, string>
}
}Services Catalog
By default, routes are grouped by first path segment.
// Auto-grouping
enableAutoDiscovery(app)
// Manual grouping with globs
enableAutoDiscovery(app, {
services: [
{ name: 'Users', description: 'User management', routes: ['/users/*'] },
{ name: 'Auth', routes: ['/auth/*'] },
],
})Registration Order and Nested Routers
- Call
enableAutoDiscovery(app)only after you register all routes and nested routers. - For nested modules, mount an
express.Router()under a base path; define child paths relative to it.
// routers/analytics.ts
import { Router } from 'express'
import { useVisionSpan } from '@getvision/adapter-express'
const router = Router()
router.get('/dashboard', (req, res) => {
const withSpan = useVisionSpan()
const analytics = withSpan('db.select', { 'db.table': 'analytics' }, () => ({ ok: true }))
res.json({ analytics })
})
export default router// index.ts
import analytics from './routers/analytics'
app.use('/analytics', analytics) // results in GET /analytics/dashboard
// enable after mounting routers
enableAutoDiscovery(app, { services: [
{ name: 'Users', routes: ['/users', '/users/*'] },
{ name: 'Analytics', routes: ['/analytics', '/analytics/*'] },
]})Zod Validation
import { z } from 'zod'
import { zValidator } from '@getvision/adapter-express'
const UpdateUser = z.object({
name: z.string().min(1).optional().describe('Full name (optional)'),
email: z.string().email().optional().describe('Email (optional)'),
})
app.put('/users/:id', zValidator('body', UpdateUser), (req, res) => {
res.json(req.body)
})The adapter attaches the Zod schema to route metadata so API Explorer can generate a commented JSON template. Comments are stripped automatically when sending.
Custom Spans
Use useVisionSpan() to create child spans attached to the current request's root span.
import { useVisionSpan } from '@getvision/adapter-express'
app.get('/users/:id', (req, res) => {
const withSpan = useVisionSpan()
const user = withSpan('db.select', { 'db.system': 'postgresql', 'db.table': 'users' }, () => {
return { id: 1, name: 'Alice' }
})
res.json(user)
})CORS
Response Headers
Vision automatically adds the X-Vision-Trace-Id header to every response. This header contains the unique trace identifier for correlating client-side metrics with server traces.
CORS Required: To read this header in the browser, you must expose it in your CORS configuration. Vision's built-in CORS handles this automatically, but if you use custom CORS:
app.use(cors({
origin: '*',
exposedHeaders: ['X-Vision-Trace-Id'], // Note: exposedHeaders for express-cors
}))
## Request Context
When `cors` is enabled (default), the middleware sets:
- `Access-Control-Allow-Origin: *`
- `Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS`
- `Access-Control-Allow-Headers: Content-Type, Authorization, X-Vision-Trace-Id, X-Vision-Session`
- `Access-Control-Expose-Headers: X-Vision-Trace-Id, X-Vision-Session`
<Callout>
If you proxy the Dashboard or run on a different origin, keep these headers to allow the API Explorer to call your API.
</Callout>
## Troubleshooting
- API Explorer shows “No schema defined” → ensure the route uses `zValidator('body', schema)` and `enableAutoDiscovery(app)` ran after route registration
- Response body shows as string → make sure the adapter is updated (response is now captured before stringify when using `res.json`)
- Spans look flat → ensure you use `useVisionSpan()` to create child spans; they will be parented under `http.request`
## Example
See repository example: `examples/express-basic/`.