@routegraph/express
v1.0.0
Published
Express v4/v5 adapter for RouteGraph — the reference implementation.
Readme
@routegraph/express
Express adapter for RouteGraph — turns a loaded RouteGraph into an Express Router.
Installation
pnpm add @routegraph/express @routegraph/core express zodexpress is a peer dependency (>=4.0.0).
createExpressRouter(graph)
function createExpressRouter(graph: RouteGraph): RouterRegisters every route from graph.getRoutes() on a new Express Router, wired through validation, middleware, and your handler.
Usage
import express from 'express'
import { RouteGraph } from '@routegraph/core'
import { createExpressRouter } from '@routegraph/express'
const graph = new RouteGraph({ routesDir: './routes' })
await graph.load()
const app = express()
app.use(express.json())
app.use('/api', createExpressRouter(graph))
// Your own error-handling middleware — required. RouteGraph forwards thrown
// handler errors to Express via next(err); Express won't produce a JSON body
// for them unless you add this.
app.use((err, _req, res, _next) => {
res.status(500).json({ error: 'Internal server error' })
})
app.listen(3000)express.json() (or an equivalent body parser) must run before the RouteGraph router — the adapter reads req.body as-is; it does not parse the request body itself.
Middleware order
For each request, in order:
graph.globalMiddleware(fromnew RouteGraph({ middleware: [...] }))- The route's own
config.middleware - Zod validation (
params/query/headers/body, whichever the route'sconfig.requestdeclares) — happens before step 1 and 2 actually run the handler, but is itself run before the middleware chain is invoked - Your route handler
Any middleware can short-circuit by writing a response and not calling next().
Validation error format
A 400 is returned automatically when any declared schema fails safeParse:
{
"error": "Validation failed",
"issues": [
{ "field": "params.id", "message": "Invalid uuid", "code": "invalid_string" }
]
}Accessing the raw Express req/res
NormalizedRequest.raw is the original Express Request object:
const handler: RouteHandler<typeof config> = async (req, res) => {
const expressReq = req.raw as import('express').Request
console.log(expressReq.ip)
}NormalizedResponse has no .raw — Express's Response isn't exposed there, since every NormalizedResponse method (.status(), .json(), ...) already maps directly onto it 1:1 inside the adapter.
Hot reload
The adapter looks up the current route via graph.getRoute(method, path) on every request rather than closing over it at registration time — so a route swapped in by @routegraph/watcher's graph.reload() takes effect immediately, without re-registering anything on the Express Router itself.
