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

@whi/cf-routing

v0.7.0

Published

Routing framework for Cloudflare Workers built on itty-router with error handling, CORS, and class-based route handlers

Readme

Cloudflare Workers Routing

License: LGPL-3.0 npm version

Class-based routing framework for Cloudflare Workers and Durable Objects built on itty-router.

Features

  • Class-based Route Handlers - Organize routes using ES6 classes
  • Context Object - Single ctx argument with request, env, params, state, and response
  • Middleware with next() - Koa/Hono-style middleware for pre/post processing
  • Built-in Error Handling - Automatic JSON error responses with proper status codes
  • CORS Support - Configurable CORS at router and handler level
  • Type Safety - Full TypeScript support with generic types
  • Durable Objects - First-class support for Durable Object routing

Installation

npm install @whi/cf-routing

Quick Start

Worker Router

import { WorkerRouter, RouteHandler, Context } from '@whi/cf-routing';

class HealthHandler extends RouteHandler {
    async get(ctx: Context) {
        return { status: 'healthy' };
    }
}

const router = new WorkerRouter()
    .defineRouteHandler('/health', HealthHandler)
    .build();

export default {
    async fetch(request, env, ctx) {
        return router.fetch(request, env, ctx);
    },
};

Durable Object Router

import { DurableObjectRouter, DurableObjectRouteHandler, DurableObjectContext } from '@whi/cf-routing';

class CounterHandler extends DurableObjectRouteHandler {
    async get(ctx: DurableObjectContext) {
        // Access storage via this.storage (flattened from DurableObjectState)
        return { count: await this.storage.get('count') || 0 };
    }

    async post(ctx: DurableObjectContext) {
        const count = await this.storage.get('count') || 0;
        await this.storage.put('count', count + 1);
        return { count: count + 1 };
    }
}

export class Counter {
    constructor(state, env) {
        this.router = new DurableObjectRouter(state, env, 'counter')
            .defineRouteHandler('/count', CounterHandler);
    }

    async fetch(request) {
        return this.router.handle(request);
    }
}

Key Features

Route Handlers

Create route handlers by extending the base classes. All handler methods receive a ctx object:

class UserHandler extends RouteHandler<Env, { id: string }> {
    async get(ctx: Context<Env, { id: string }>) {
        return { userId: ctx.params.id };
    }

    async post(ctx: Context<Env, { id: string }>) {
        const body = await ctx.request.json();
        return { userId: ctx.params.id, created: true };
    }
}

router.defineRouteHandler('/users/:id', UserHandler);

The ctx object contains:

  • ctx.request - The incoming Request
  • ctx.env - Environment bindings (Worker handlers only)
  • ctx.params - Route parameters (e.g., { id: '123' })
  • ctx.data - Shared data for middleware communication
  • ctx.response - Response customization (status, headers)
  • ctx.log - Logger instance

For DurableObject handlers, the handler instance also has:

  • this.storage - DurableObjectStorage (flattened from state)
  • this.id - DurableObjectId
  • this.state - Raw DurableObjectState (for blockConcurrencyWhile, etc.)
  • this.env - Environment bindings

Automatic Error Handling

Throw HttpError for proper HTTP status codes:

import { HttpError } from '@whi/cf-routing';

async get(ctx: Context<Env, { id: string }>) {
    if (!ctx.params?.id) {
        throw new HttpError(400, 'ID required');
    }
    // Errors automatically become JSON responses
}

Response Customization

Customize status codes and headers via ctx.response:

async post(ctx: Context) {
    ctx.response.status = 201;
    ctx.response.headers.set('Set-Cookie', 'session=abc123');
    return { created: true };
}

Or return a Response directly for full control:

async get(ctx: Context) {
    return new Response('<html>...</html>', {
        headers: { 'Content-Type': 'text/html' }
    });
}

Middleware with next()

Middleware uses the Koa/Hono-style next() pattern for pre/post processing:

import { Middleware } from '@whi/cf-routing';

const authMiddleware: Middleware<Env> = async (ctx, next) => {
    // Pre-processing
    const token = ctx.request.headers.get('Authorization');
    if (!token) {
        throw new HttpError(401, 'Unauthorized');
    }
    ctx.data.userId = validateToken(token);

    // Call next middleware/handler
    const response = await next();

    // Post-processing (optional)
    return response;
};

router
    .use(authMiddleware)                    // Global middleware
    .use('/api/*', rateLimitMiddleware)     // Path-specific middleware
    .defineRouteHandler('/api/users', UserHandler);

For DurableObject middleware, the signature is (ctx, state, next) where state is the DurableObjectState:

import { DurableObjectMiddleware } from '@whi/cf-routing';

const sessionMiddleware: DurableObjectMiddleware = async (ctx, state, next) => {
    const session = await state.storage.get('session');
    ctx.data.session = session;
    return next();
};

CORS Support

Configure CORS at the router level or per-handler with dynamic control:

// Router-level CORS (applies to all handlers without their own cors())
const router = new WorkerRouter<Env>('api', {
    cors: { origins: '*' }
});

// Per-handler dynamic CORS
class ApiHandler extends RouteHandler<Env> {
    cors(ctx: Context<Env>) {
        const origin = ctx.request.headers.get('Origin');
        // Allow specific subdomains
        if (origin?.endsWith('.myapp.com')) {
            return { origins: origin, credentials: true };
        }
        return undefined; // Use router default
    }

    async get(ctx: Context<Env>) {
        return { data: 'hello' };
    }
}

Dynamic Origins from Environment Variables

For Cloudflare Workers, allowed origins are often configured via environment variables. Use a function for origins to access env and middleware-set data:

const router = new WorkerRouter<Env>('api', {
    cors: {
        origins: ({ request, env, data }) => {
            const origin = request.headers.get('Origin');
            const allowed = env.ALLOWED_ORIGINS?.split(',') || [];
            return origin && allowed.includes(origin) ? origin : null;
        },
        credentials: true,
    }
});

The function receives:

  • request - The incoming Request
  • env - Environment bindings (secrets, KV, etc.)
  • data - Middleware-set data (useful if middleware determines allowed origins)

Middleware on OPTIONS Requests

Middleware runs for all requests including OPTIONS preflight. This enables rate limiting, logging, and authentication checks on preflight requests:

router.use(async (ctx, next) => {
    // This runs for GET, POST, OPTIONS, etc.
    console.log(`${ctx.request.method} ${ctx.request.url}`);
    return next();
});

CORS headers are automatically consistent between OPTIONS preflight and actual responses.

Logging

The router includes a built-in Logger that integrates with Cloudflare's observability dashboard. Set the log level via the LOG_LEVEL environment variable.

Log Levels (from most to least verbose):

  • trace - Detailed request flow (incoming request, middleware chain, handler execution)
  • debug - Route matching with params
  • info - Request completed with status and duration
  • warn - Warnings
  • error - Errors
  • fatal - Critical errors

Built-in Logging

The router automatically logs at these levels:

[router-name] [TRACE] Incoming request {"method":"GET","path":"/users/123"}
[router-name] Route matched {"path":"/users/:id","params":{"id":"123"}}
[router-name] [TRACE] Middleware chain {"count":2}
[router-name] [TRACE] Executing handler {"method":"get"}
[router-name] Request completed {"method":"GET","path":"/users/123","status":200,"duration":45}

Using the Logger

Access ctx.log in handlers and middleware with structured data:

async get(ctx: Context<Env, { id: string }>) {
    ctx.log.info('Fetching user', { userId: ctx.params.id });

    const user = await getUser(ctx.params.id);
    if (!user) {
        ctx.log.warn('User not found', { userId: ctx.params.id });
        throw new HttpError(404, 'User not found');
    }

    return user;
}

Configuration

Set LOG_LEVEL in your wrangler.toml:

[vars]
LOG_LEVEL = "info"  # or "debug", "trace", etc.

Documentation

https://webheroesinc.github.io/js-cf-routing/

API documentation is automatically generated from source code using TypeDoc and deployed on every push to master.

To generate locally:

npm run docs         # Generate documentation in docs/
npm run docs:watch   # Generate docs in watch mode

Development

See CONTRIBUTING.md for development setup, testing, and contribution guidelines.

Running Tests

npm test                 # Run all tests
npm run test:unit        # Unit tests only
npm run test:integration # Integration tests only
npm run test:coverage    # With coverage report

Building

npm run build            # Build TypeScript to lib/
npm run format           # Format code with Prettier

License

LGPL-3.0

Credits

Built on top of itty-router by Kevin Whitley.