The Challenge π¨
I recently built BEnder, a framework-agnostic boilerplate that runs on Express, Fastify, Hono, or Elysia using a single codebase.
Adding Koa is another to single truth concept.
The Problem: Context vs. Req/Res
Most Node frameworks (Express, Fastify) follow the standard callback signature:
(req, res) => void
Koa, however, uses a Context object:
(ctx, next) => Promise<void>
This broke my unified IRequest and IResponse interfaces. My "Synapses" (route handlers) expected to call res.json(), but Koa expects you to set ctx.body.
The Solution: The Adapter Pattern π§©
I wrote a KoaRouterShim that wraps every handler. It creates a proxy object for res that translates native generic methods into Koa property assignments.
The Wrapper Code
Here is the magic that makes Koa behave like Express:
// KoaRouterShim.ts
private wrap(method: string, path: string, handler: Function) {
this.router[method](path, async (ctx: any, next: any) => {
// 1. Adapt Request
// We merge ctx.request, ctx.params, and ctx.query into one 'IRequest'
const req = { ...ctx.request, params: ctx.params, query: ctx.query };
// 2. Adapt Response (The trick!)
// We create a proxy that mimics Express methods but manipulates Koa state
const res = {
status: (code) => {
ctx.status = code;
return res;
}, // Chainable
json: (data) => {
ctx.body = data;
return res;
}, // Sets body
send: (data) => {
ctx.body = data;
return res;
}
};
// 3. Execute the standard handler
await handler(req, res);
});
}
Now, my business logic remains 100% reusable:
// Works on Koa, Express, Fastify, Hono...
this.router.get('/hello', (req, res) => {
res.status(200).json({ message: "Hello from Koa!" });
});
Why This Matters
By abstracting the framework layer, we stop fighting "Framework Lock-in". Today I run on Node+Fastify. Tomorrow, if Koa releases a killer feature, I'm installing it, and my app keep on running.
Check out the full implementation in BEnder!
Top comments (0)