DEV Community

Adam Golan
Adam Golan

Posted on

I Added Koa Support to My Universal Boilerplate (And It Was Tricky)

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
Enter fullscreen mode Exit fullscreen mode

Koa, however, uses a Context object:

(ctx, next) => Promise<void>
Enter fullscreen mode Exit fullscreen mode

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);
    });
}
Enter fullscreen mode Exit fullscreen mode

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!" });
});
Enter fullscreen mode Exit fullscreen mode

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)