Route Files & Validation
RouteConfig
interface RouteConfig {
description?: string
tags?: string[]
deprecated?: boolean
middleware?: Middleware[]
request?: {
params?: ZodType
query?: ZodType
body?: ZodType
headers?: ZodType
}
response?: {
[statusCode: number]: ZodType
}
}Every field is optional. An empty {} — or no config export at all — is
a valid, unvalidated route.
satisfies, not as
export const config = {
request: { params: z.object({ id: z.string() }) },
} satisfies RouteConfigsatisfies checks the object against RouteConfig without widening its
type — RouteHandler<typeof config> can then infer req.params.id as
string, not unknown. as RouteConfig would compile but throw away that
inference; a plain : RouteConfig annotation would widen the object to the
interface and lose the same information. Use satisfies.
RouteHandler
const handler: RouteHandler<typeof config> = async (req, res) => {
// req.params, req.query, req.body — typed from `config.request`
}
export default handlerThe generic reads config’s Zod schemas and produces the exact request
shape your handler sees — this is the mechanism that makes
req.params.id a string instead of string | undefined, with zero
manual typing.
Validation, field by field
Add a schema to any of params, query, body, headers independently —
validate only what you need:
export const config = {
request: {
body: z.object({ name: z.string().min(1), email: z.string().email() }),
},
response: {
201: z.object({ id: z.string(), name: z.string(), email: z.string() }),
},
} satisfies RouteConfigA request that fails validation never reaches your handler. It gets a
uniform 400:
{
"error": "Validation failed",
"issues": [
{ "field": "body.email", "message": "Invalid email", "code": "invalid_string" }
]
}This shape is identical across all five adapters. Your error-handling code on the client doesn’t need to know which framework is running the server.
Custom error handling
Pass onError where you construct the adapter to override the default
400 shape:
createExpressRouter(graph, {
onError: (issues, req, res) => {
res.status(422).json({ code: 'VALIDATION_ERROR', issues })
},
})Tags, description, deprecated
export const config = {
description: 'List all users, optionally filtered by role',
tags: ['users', 'admin'],
deprecated: false,
} satisfies RouteConfigtags group routes in the docs UI. deprecated: true
marks a route visually in the docs — it does not remove it or change its
runtime behavior.
Middleware on a route
export const config = {
middleware: [requireAuth, rateLimit({ max: 100 })],
} satisfies RouteConfigRoute-level middleware runs after global (app-level) middleware and before validation — see the middleware chain.
A complete annotated example
import { z } from 'zod'
import type { RouteConfig, RouteHandler } from '@routegraph/core'
export const config = {
description: 'Get a single user by their ID',
tags: ['users'],
request: {
params: z.object({ id: z.string() }),
},
response: {
200: z.object({ id: z.string(), name: z.string(), email: z.string().email() }),
404: z.object({ error: z.string() }),
},
} satisfies RouteConfig
const handler: RouteHandler<typeof config> = async (req, res) => {
const user = findUserById(req.params.id)
if (!user) return res.status(404).json({ error: 'User not found' })
res.status(200).json(user)
}
export default handler