Look at your codebase right now.
If you are building an open-source library, an SDK, or a generic backend tool, and you are importing express, next/server, or hono directly into your core business logic... you are building a trap.
I know, because I fell into it. When you tie your logic to a specific HTTP framework, you alienate 80% of the developer ecosystem. A Next.js dev can't use your Express tool. A Hono dev can't use your Fastify plugin.
If you want to build tools that other developers actually adopt, you have to think like an architect, not just a coder.
You need to master The Engine-Adapter Pattern.
This is exactly how I built TableCraft to seamlessly support Hono, Express, Next.js, and Elysia from a single codebase. Let's break down how you can build this pattern for your next open-source project.
🛑 The Anti-Pattern: Framework Coupling
Most developers start building an API library like this:
// ❌ BAD: Tightly coupled to Express
import { Request, Response } from 'express';
export function myAwesomeLibrary(req: Request, res: Response) {
const query = req.query.search;
// ... core logic ...
return res.json({ success: true, data });
}
The moment you do this, your library is dead to anyone not using Express.
🏗️ The Solution: The Engine-Adapter Architecture
To fix this, you must aggressively separate your Core Engine from the HTTP Layer.
Your architecture should look like this:
- The Engine: Pure TypeScript. Zero dependencies on any HTTP framework. It takes standard inputs (strings, objects) and returns standard outputs.
- The Adapters: Tiny, 20-line wrappers that translate a specific framework's request into the standard input your Engine expects.
Let's look at how to actually code this.
Step 1: Build the Framework-Agnostic Engine
Your engine should only care about raw data. It doesn't know what a "Request" or "Response" is.
// packages/engine/src/index.ts
// ✅ GOOD: Pure TypeScript. No HTTP context.
export interface EngineContext {
url: string;
method: string;
body?: any;
}
export class CoreEngine {
async handleRequest(ctx: EngineContext) {
// 1. Parse the raw URL
const url = new URL(ctx.url, 'http://localhost');
const searchParams = url.searchParams;
// 2. Execute pure business logic
const result = await myComplexLogic(searchParams);
// 3. Return a standard response object
return {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result)
};
}
}
Step 2: Build the Adapters
Now, we create isolated packages for each framework. These are the "Adapters". Their only job is translation.
The Hono Adapter:
// packages/adapter-hono/src/index.ts
import { Context } from 'hono';
import { CoreEngine } from '@my-org/engine';
export function createHonoAdapter(engine: CoreEngine) {
return async (c: Context) => {
// Translate Hono Request -> Engine Context
const response = await engine.handleRequest({
url: c.req.url,
method: c.req.method,
});
// Translate Engine Response -> Hono Response
return c.json(JSON.parse(response.body), response.status);
};
}
The Express Adapter:
// packages/adapter-express/src/index.ts
import { Request, Response } from 'express';
import { CoreEngine } from '@my-org/engine';
export function createExpressAdapter(engine: CoreEngine) {
return async (req: Request, res: Response) => {
// Translate Express Request -> Engine Context
const response = await engine.handleRequest({
url: req.originalUrl,
method: req.method,
});
// Translate Engine Response -> Express Response
res.status(response.status).json(JSON.parse(response.body));
};
}
🧠 Why This Changes Everything
When you build like this, magic happens:
- Infinite Reach: Adding support for a new framework (like SvelteKit or Nuxt) takes 10 minutes. You just write a new 20-line adapter.
-
God-Tier Testing: You can unit test your
CoreEngineperfectly without ever spinning up a mock HTTP server. You just pass it JSON objects. -
Monorepo Ready: You can use Turborepo or npm workspaces to publish
@my-org/engine,@my-org/adapter-hono, and@my-org/adapter-nextas separate packages. Users only install what they need.
🚀 See It In The Wild
This isn't theory. This is exactly the architecture I used to build TableCraft — a headless data table engine for Drizzle ORM.
I wanted TableCraft to work for everyone. So I built @tablecraft/engine as a pure logic core, and exposed it via @tablecraft/adapter-hono, @tablecraft/adapter-express, etc.
If you are a creator, a library author, or just someone who wants to understand how scalable open-source repositories are structured, I highly recommend studying this pattern in a real codebase.
👉 Study the Monorepo Architecture in TableCraft on GitHub (jacksonkasi1/TableCraft)
If you are building an open-source tool right now, what is the hardest architectural decision you are currently facing? Let's talk in the comments.
Top comments (0)