@routegraph/elysia
v1.0.0
Published
Elysia adapter for RouteGraph, Bun-native (Node via @elysiajs/node).
Readme
@routegraph/elysia
Elysia adapter for RouteGraph — Bun-native, but also runnable on Node via @elysiajs/node.
Installation
pnpm add @routegraph/elysia @routegraph/core elysia zodelysia is a peer dependency (>=1.0.0). If you want to run under plain Node (as this monorepo's own example does) rather than Bun, also install @elysiajs/node and pass it as Elysia's adapter.
createElysiaPlugin(graph)
function createElysiaPlugin(graph: RouteGraph): Promise<Elysia>This is async — unlike every other adapter's synchronous factory, you must await it before mounting. See why below.
Usage
import { Elysia } from 'elysia'
import { node } from '@elysiajs/node' // omit this + the adapter option to run on Bun directly
import { RouteGraph } from '@routegraph/core'
import { createElysiaPlugin } from '@routegraph/elysia'
const graph = new RouteGraph({ routesDir: './routes' })
await graph.load()
const app = new Elysia({ adapter: node() })
const apiPlugin = await createElysiaPlugin(graph)
app.group('/api', (group) => group.use(apiPlugin))
app.onError(({ error, set }) => {
set.status = 500
return { error: 'Internal server error' }
})
app.listen(3000)Elysia response pattern explanation
Every other RouteGraph adapter's framework lets you write a response imperatively — Express's res.json(data), Fastify's reply.send(data), Koa's ctx.body = data all take effect the moment they're called. Elysia's own convention is the opposite: a handler's return value becomes the response, and there's no "send now" primitive to call from inside RouteGraph's cross-adapter NormalizedResponse contract (.status(), .json(), .send(), .setHeader(), .end() — all synchronous, void-returning, and callable more than once, e.g. res.status(404).json(...)).
ElysiaResponseCollector (in src/adapter.ts) implements NormalizedResponse by buffering whatever status/body/headers your handler (or the adapter's own validation-failure/error paths) set, instead of writing them anywhere immediately. Once your handler and the middleware chain have both finished, the plugin's request wrapper applies the buffered status/headers to ctx.set and returns the buffered body as the handler's actual return value — which is what makes it appear in Elysia's response. This buffering is also why createElysiaPlugin itself has to be async: constructing the plugin and wiring up its per-route handlers needs to happen before .listen(), but the response-collection logic runs per-request, independent of that.
See DECISIONS.md for the full reasoning.
Middleware adaptation notes
Middleware functions ((req, res, next) => Promise<void>) work unmodified — they call res.status()/res.json() on the same ElysiaResponseCollector instance the handler receives, so a middleware that short-circuits by not calling next() still ends up producing the right buffered response.
Validation error format
{ "error": "Validation failed", "issues": [{ "field": "body.name", "message": "String must contain at least 1 character(s)", "code": "too_small" }] }Docs UI
routegraph dev's automatic docs-UI mounting is not implemented for Elysia yet — see examples/with-express/index.elysia.ts for the manual bridging pattern used there (wrapping createDocsMiddleware's Connect-style signature to return a real Response).
