DEV Community

Cover image for How to Build Framework-Agnostic Open Source Tools (The Engine-Adapter Pattern)
Jackson Kasi
Jackson Kasi Subscriber

Posted on

How to Build Framework-Agnostic Open Source Tools (The Engine-Adapter Pattern)

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

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:

  1. The Engine: Pure TypeScript. Zero dependencies on any HTTP framework. It takes standard inputs (strings, objects) and returns standard outputs.
  2. 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)
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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

🧠 Why This Changes Everything

When you build like this, magic happens:

  1. Infinite Reach: Adding support for a new framework (like SvelteKit or Nuxt) takes 10 minutes. You just write a new 20-line adapter.
  2. God-Tier Testing: You can unit test your CoreEngine perfectly without ever spinning up a mock HTTP server. You just pass it JSON objects.
  3. Monorepo Ready: You can use Turborepo or npm workspaces to publish @my-org/engine, @my-org/adapter-hono, and @my-org/adapter-next as 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)