Skip to content

Instantly share code, notes, and snippets.

@Sleavely
Last active March 22, 2026 11:50
Show Gist options
  • Select an option

  • Save Sleavely/bd00ddf4a6f76d038881ea46d744a808 to your computer and use it in GitHub Desktop.

Select an option

Save Sleavely/bd00ddf4a6f76d038881ea46d744a808 to your computer and use it in GitHub Desktop.
A simple Express-style router using URLPattern API introduced in NodeJS 24

A simple Express-style router using URLPattern API introduced in NodeJS 24.

The example below will work in a AWS Lambda environment.

First, register the routes:

const router = new URLPatternRouter()
router.get('/api/hello', async (event: APIGatewayProxyEventV2) => {
  return import('./hello/GET.js').then(({ handler }) => handler(event))
})

Then, in your handler function:

  const route = router.match(event.requestContext.http.method, event.rawPath)
  if (!route) {
    const response = {
      statusCode: 404,
      body: JSON.stringify({ message: 'Not Found' }),
    }
    Logger.debug('Response', response)
    return response
  }

  // Reassign the path parameters in the format expected by the route rather than API Gateway
  event.pathParameters = route.pathParameters

  // Run the handler.
  const response = await route.handler(event)

  // If the response body isnt JSON parseable, make it so
  const contentTypeShouldBeJson = typeof response === 'string' ||
    !response.headers?.['Content-Type'] ||
    response.headers?.['Content-Type'] === 'application/json';
  if (typeof response !== 'string' && response.body && contentTypeShouldBeJson) {
    try {
      JSON.parse(response.body);
    } catch {
      response.body = JSON.stringify(response.body);
    }
  }

  return response;
import { describe, it, expect, vi } from 'vitest';
import { URLPatternRouter } from './URLPatternRouter.js';
describe('URLPatternRouter', () => {
it('should match a simple route', async () => {
const router = new URLPatternRouter();
router.get('/hello', (): any => 'Hello World');
const route = router.match('GET', '/hello');
expect(route).not.toBeNull();
const result = await route!.handler({} as any);
expect(result).toBe('Hello World');
});
it('returns null for unmatched routes', async () => {
const router = new URLPatternRouter();
router.get('/hello', (): any => 'Hello World');
const route = router.match('GET', '/goodbye');
expect(route).toBeNull();
});
it('cares about method', async () => {
const router = new URLPatternRouter();
router.post('/submit', (): any => 'Submitted');
const routeGet = router.match('GET', '/submit');
const routePost = router.match('POST', '/submit');
const responsePost = await routePost!.handler({} as any);
expect(routeGet).toBeNull();
expect(responsePost).toBe('Submitted');
});
it('ignores method for "any" routes', async () => {
const handler = vi.fn((): any => 'Potato');
const router = new URLPatternRouter();
router.any('/fruit', handler);
const routeGet = router.match('GET', '/fruit');
const responseGet = await routeGet!.handler({} as any);
const routePost = router.match('POST', '/fruit');
const responsePost = await routePost!.handler({} as any);
expect(handler).toHaveBeenCalledTimes(2);
expect(responseGet).toBe('Potato');
expect(responsePost).toBe('Potato');
});
it('should extract parameters from the route', async () => {
const handlerFn = vi.fn((params): any => {});
const router = new URLPatternRouter();
router.get('/user/:id', handlerFn);
const route = router.match('GET', '/user/42');
expect(route?.pathParameters).toMatchObject({ id: '42' });
});
});
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
type Handler = (event: APIGatewayProxyEventV2) => Promise<APIGatewayProxyResultV2>;
interface RouteResponse {
handler: Handler;
pathParameters: URLPatternResult['pathname']['groups'];
}
export class URLPatternRouter {
private routes: Array<{ pattern: URLPattern, handler: Handler }> = []
private addPattern (method: string | undefined, path: string, handler: Handler): void {
this.routes.push({ pattern: new URLPattern({ protocol: method, pathname: path }), handler });
}
public match (method: string, path: string): RouteResponse | null {
const route = this.routes.find(({ pattern }) => pattern.test({ protocol: method, pathname: path }));
if (!route) {
return null;
}
const patternMatch = route.pattern.exec({ protocol: method, pathname: path });
return {
handler: route.handler,
pathParameters: patternMatch!.pathname.groups,
};
}
head (path: string, handler: Handler) {
this.addPattern('HEAD', path, handler);
}
options (path: string, handler: Handler) {
this.addPattern('OPTIONS', path, handler);
}
get (path: string, handler: Handler) {
this.addPattern('GET', path, handler);
}
post (path: string, handler: Handler) {
this.addPattern('POST', path, handler);
}
put (path: string, handler: Handler) {
this.addPattern('PUT', path, handler);
}
delete (path: string, handler: Handler) {
this.addPattern('DELETE', path, handler);
}
patch (path: string, handler: Handler) {
this.addPattern('PATCH', path, handler);
}
any (path: string, handler: Handler) {
this.addPattern(undefined, path, handler);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment