npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

ctx-router

v1.0.8

Published

Framework-agnostic router for building portable APIs. Write your business logic once, run it on Express, Lambda, SQS, gRPC, or any transport layer.

Readme

ctx-router

npm version License: MIT

Write your business logic once. Run it on Express, Fastify, Lambda, SQS, gRPC, or anything else.

ctx-router is a framework-agnostic router that normalizes all transport layers (HTTP frameworks, serverless functions, event streams, gRPC) into a unified context. Your business logic stays the same regardless of how requests arrive.

The Problem

Building modern applications often requires supporting multiple transport layers:

  • Start with Express, then want to try Fastify or Koa → Need to rewrite handlers
  • Deploy to AWS Lambda → Need to rewrite request/response handling
  • Add SQS or Kinesis processing → Need different code for events vs HTTP
  • Switch from AWS to Google Cloud Functions or Azure Functions → Vendor lock-in
  • Support gRPC alongside HTTP → Maintain duplicate logic

Existing solutions only solve part of this:

  • Framework adapters (serverless-express, etc.) only handle HTTP → Lambda
  • NestJS supports multiple transports but requires heavy framework buy-in
  • You end up with transport-specific code scattered everywhere

The Solution

ctx-router sits between incoming requests and your business logic, providing:

Without ctx-router:

Express Handler → Your Logic
Lambda Handler → Rewritten Logic
SQS Handler → More Rewritten Logic
gRPC Handler → Even More Rewritten Logic

With ctx-router:

Express → toCtx.fromExpress() ┐
Lambda → toCtx.fromLambda()   ├→ Unified Context → Your Logic (once!)
SQS → toCtx.fromSQS()         │
gRPC → toCtx.fromGRPC()       ┘

Key Benefits

  • Framework-agnostic: Switch from Express to Fastify to Koa without touching business logic
  • Cloud-agnostic: Same code works on AWS Lambda, Google Cloud Functions, Azure Functions
  • Multi-transport: HTTP, events (SQS, Kinesis), gRPC, WebSockets all normalized
  • Lightweight: Not a framework, just a routing layer (~10KB)
  • Type-safe: Full TypeScript support with generic context types
  • Clean Architecture: Separates transport concerns from business logic

Installation

npm install ctx-router

Or using pnpm:

pnpm add ctx-router

Quick Start

1. Define Your Router (Once)

router.ts - Your centralized, transport-agnostic router

import { CtxRouter, TDefaultCtx } from "ctx-router";
import * as api from "./api";

// Extend the default context with your app's requirements
export type TCtx = TDefaultCtx & {
  user: {
    id: string;
    role: string[];
  };
};

// Create your router
export const router = new CtxRouter<TCtx>();

// Define routes once
router.handle("GET", "/health/ping", api.health.ping);
router.handle("POST", "/user/update", api.user.update);
router.handle("GET", "/user/:id", api.user.detail);

// Global error handler
router.onError(async (ctx, error) => {
  console.error("Route error:", error);
  ctx.res.code = "ERROR";
  ctx.res.msg = "Something went wrong";
  return ctx;
});

2. Write Your Business Logic (Once)

api/user/userUpdate.api.ts - Handler that works everywhere

import { TCtx } from "../../router";

// Authentication - works regardless of transport
export async function auth(ctx: TCtx): Promise<TCtx> {
  // Your auth logic here (JWT validation, etc.)
  if (!ctx.user || !ctx.user.role.includes("USER")) {
    throw new Error("Unauthorized");
  }
  return ctx;
}

// Validation - transport-agnostic
export async function validate(ctx: TCtx): Promise<TReqData> {
  const data = ctx.req.data as TReqData;
  // Your validation logic
  return data;
}

// Business logic - pure, no transport concerns
export async function execute(reqData: TReqData): Promise<TResData> {
  return {
    userId: reqData.userId,
    userName: reqData.userName,
    updatedAt: new Date().toISOString(),
  };
}

type TReqData = { userId: string; userName: string };
type TResData = { userId: string; userName: string; updatedAt: string };

3. Connect Any Transport Layer

Express.js

express.ts - 10 lines to connect Express

import express, { Request, Response } from "express";
import { toCtx } from "ctx-router";
import { router, TCtx } from "./router";

const app = express();
app.use(express.json());

function getHttpCode(ctx: TCtx) {
  if (ctx.res.code === "OK") return 200;
  if (ctx.res.code === "UNKNOWN_ERROR") return 500;
  return 400;
}

app.all("*", async (req: Request, res: Response) => {
  const ctx: TCtx = toCtx.fromExpress(req);
  await router.exec(ctx);
  res.status(getHttpCode(ctx)).json(ctx.res);
});

app.listen(3000, () => console.log("Server running on port 3000"));

AWS Lambda

lambda.ts - Same business logic, different transport

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { toCtx } from "ctx-router";
import { router, TCtx } from "./router";

export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  const ctx: TCtx = toCtx.fromLambda(event);
  await router.exec(ctx);

  return {
    statusCode: ctx.res.code === "OK" ? 200 : 400,
    body: JSON.stringify(ctx.res),
  };
};

AWS SQS

sqs.ts - Process events with the same handlers

import { SQSEvent } from "aws-lambda";
import { toCtx } from "ctx-router";
import { router, TCtx } from "./router";

export const handler = async (event: SQSEvent): Promise<void> => {
  for (const record of event.Records) {
    const ctx: TCtx = toCtx.fromSQS(record);
    await router.exec(ctx);
    // Process result as needed
  }
};

Fastify (or Koa, Hapi, etc.)

fastify.ts - Switch frameworks without touching business logic

import Fastify from "fastify";
import { toCtx } from "ctx-router";
import { router, TCtx } from "./router";

const fastify = Fastify();

fastify.all("*", async (request, reply) => {
  const ctx: TCtx = toCtx.fromFastify(request);
  await router.exec(ctx);
  reply.status(ctx.res.code === "OK" ? 200 : 400).send(ctx.res);
});

fastify.listen({ port: 3000 });

Architecture

Context Structure

The unified context (TCtx) contains everything your business logic needs:

type TDefaultCtx = {
  req: {
    method: string; // GET, POST, etc.
    path: string; // /user/123
    query: Record<string, any>;
    params: Record<string, any>;
    headers: Record<string, any>;
    data: any; // Body/payload
  };
  res: {
    code: string; // OK, ERROR, etc.
    msg: string;
    data: any;
  };
  meta: {
    log: {
      stdout: string[];
    };
  };
  user: any; // Extend with your user type
};

Handler Structure

Handlers follow a consistent pattern:

export async function auth(ctx: TCtx): Promise<TCtx> {
  // Authenticate request, populate ctx.user
  return ctx;
}

export async function validate(ctx: TCtx): Promise<TReqData> {
  // Validate and transform ctx.req.data
  return validatedData;
}

export async function execute(reqData: TReqData): Promise<TResData> {
  // Pure business logic, no context needed
  return result;
}

Advanced Features

Custom Error Types

import { ctxErrMap } from "ctx-router";

export const ctxErr = ctxErrMap({
  general: {
    UNKNOWN_ERROR: "Something went wrong",
    NOT_FOUND: "Resource not found",
  },
  auth: {
    UNAUTHORIZED: "Unauthorized",
    TOKEN_EXPIRED: "Token expired",
  },
});

// Use in handlers
throw ctxErr.auth.UNAUTHORIZED();

Logging

const router = new CtxRouter<TCtx>({
  log: { capture: true },
});

// Logs are captured in ctx.meta.log.stdout
router.logConsole("Processing request");

Redis Streaming

import { createClient } from "@redis/client";

const redisClient = createClient();
await redisClient.connect();

const router = new CtxRouter<TCtx>({
  stream: {
    redisClient,
    key: "CTX:OBJ",
  },
});

// Context is automatically streamed to Redis
await router.flushToStream(ctx);

Role-Based Authorization

export const USER_ROLE = {
  USER: "USER",
  ADMIN: "ADMIN",
  SERVER: "SERVER",
} as const;

export type TCtx = TDefaultCtx & {
  user: {
    role: Array<keyof typeof USER_ROLE>;
  };
};

// In your handler
export async function auth(ctx: TCtx): Promise<TCtx> {
  const allowedRoles = [USER_ROLE.ADMIN, USER_ROLE.USER];
  if (!ctx.user.role.some((r) => allowedRoles.includes(r))) {
    throw ctxErr.auth.UNAUTHORIZED();
  }
  return ctx;
}

Use Cases

Migrating Frameworks

Start with Express, migrate to Fastify later without rewriting business logic.

Multi-Cloud Deployment

Deploy the same code to AWS Lambda, Google Cloud Functions, and Azure Functions.

Hybrid Architecture

Serve HTTP requests via Express and process async jobs via SQS with the same handlers.

Microservices

Support HTTP, gRPC, and message queues without duplicating logic.

Testing

Write tests against the unified context without mocking framework-specific objects.

API Reference

CtxRouter

Constructor

new CtxRouter<TCtx>(options?: {
  log?: { capture: boolean };
  stream?: { redisClient: RedisClient; key: string };
})

Methods

  • handle(method: string, path: string, handler: IBaseApi<TCtx>) - Register a route
  • exec(ctx: TCtx): Promise<TCtx> - Execute a route
  • onError(handler: (ctx: TCtx, error: unknown) => Promise<TCtx>) - Set error handler
  • logConsole(message: string) - Log a message
  • logGetRef() - Get captured logs
  • flushToStream(ctx: TCtx) - Stream context to Redis

Context Converters (toCtx)

  • toCtx.fromExpress(req: Request) - Convert Express request
  • toCtx.fromLambda(event: APIGatewayProxyEvent) - Convert Lambda event
  • toCtx.fromSQS(record: SQSRecord) - Convert SQS record
  • toCtx.fromFastify(request: FastifyRequest) - Convert Fastify request
  • Custom converters can be created for any transport

Examples

See the /src/example directory for complete working examples.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT - Kaushik R Bangera

Links