@koa/router
v15.0.0
Published
Router middleware for koa. Maintained by Forward Email and Lad.
Readme
@koa/router
Modern TypeScript Router middleware for Koa. Maintained by Forward Email and Lad.
Table of Contents
- Features
- Installation
- TypeScript Support
- Quick Start
- API Documentation
- Advanced Features
- Best Practices
- Recipes
- Performance
- Testing
- Migration Guides
- Contributing
- License
- Contributors
Features
- ✅ Full TypeScript Support - Written in TypeScript with comprehensive type definitions
- ✅ Express-Style Routing - Familiar
app.get,app.post,app.put, etc. - ✅ Named URL Parameters - Extract parameters from URLs
- ✅ Named Routes - Generate URLs from route names
- ✅ Host Matching - Match routes based on hostname
- ✅ HEAD Request Support - Automatic HEAD support for GET routes
- ✅ Multiple Middleware - Chain multiple middleware functions
- ✅ Nested Routers - Mount routers within routers
- ✅ RegExp Paths - Use regular expressions for flexible path matching
- ✅ Parameter Middleware - Run middleware for specific URL parameters
- ✅ Path-to-RegExp v8 - Modern, predictable path matching
- ✅ 405 Method Not Allowed - Automatic method validation
- ✅ 501 Not Implemented - Proper HTTP status codes
- ✅ Async/Await - Full promise-based middleware support
Installation
npm:
npm install @koa/routeryarn:
yarn add @koa/routerRequirements:
- Node.js >= 20 (tested on v20, v22, v24, v25)
- Koa >= 2.0.0
TypeScript Support
@koa/router is written in TypeScript and includes comprehensive type definitions out of the box. No need for @types/* packages!
Basic Usage
import Router, { RouterContext } from '@koa/router';
const router = new Router();
// Fully typed context
router.get('/:id', (ctx: RouterContext, next) => {
const id = ctx.params.id; // Type-safe parameters
ctx.body = { id };
});Generic Types
The router supports generic type parameters for full type safety with custom state and context types:
import Router, { RouterContext } from '@koa/router';
import type { Next } from 'koa';
// Define your application state
interface AppState {
user?: {
id: string;
email: string;
};
}
// Define your custom context
interface AppContext {
requestId: string;
}
// Create router with generics
const router = new Router<AppState, AppContext>();
// Type-safe route handlers
router.get(
'/profile',
(ctx: RouterContext<AppState, AppContext>, next: Next) => {
// ctx.state.user is fully typed
if (ctx.state.user) {
ctx.body = {
user: ctx.state.user,
requestId: ctx.requestId // Custom context property
};
}
}
);Extending Types in Route Handlers
HTTP methods support generic type parameters to extend state and context types:
interface UserState {
user: { id: string; name: string };
}
interface UserContext {
permissions: string[];
}
// Extend types for specific routes
router.get<UserState, UserContext>(
'/users/:id',
async (ctx: RouterContext<UserState, UserContext>) => {
// ctx.state.user is fully typed
// ctx.permissions is fully typed
ctx.body = {
user: ctx.state.user,
permissions: ctx.permissions
};
}
);Parameter Middleware Types
import type { RouterParameterMiddleware } from '@koa/router';
import type { Next } from 'koa';
// Type-safe parameter middleware
router.param('id', ((value: string, ctx: RouterContext, next: Next) => {
if (!/^\d+$/.test(value)) {
ctx.throw(400, 'Invalid ID format');
}
return next();
}) as RouterParameterMiddleware);Available Types
import {
Router,
RouterContext,
RouterOptions,
RouterMiddleware,
RouterParameterMiddleware,
RouterParamContext,
AllowedMethodsOptions,
UrlOptions,
HttpMethod
} from '@koa/router';
import type { Next } from 'koa';
// Router with generics
type MyRouter = Router<AppState, AppContext>;
// Context with generics
type MyContext = RouterContext<AppState, AppContext, BodyType>;
// Middleware with generics
type MyMiddleware = RouterMiddleware<AppState, AppContext, BodyType>;
// Parameter middleware with generics
type MyParamMiddleware = RouterParameterMiddleware<
AppState,
AppContext,
BodyType
>;Type Safety Features
- ✅ Full generic support -
Router<StateT, ContextT>for custom state and context types - ✅ Type-safe parameters -
ctx.paramsis fully typed - ✅ Type-safe state -
ctx.staterespects your state type - ✅ Type-safe middleware - Middleware functions are fully typed
- ✅ Type-safe HTTP methods - Methods support generic type extensions
- ✅ Compatible with @types/koa-router - Matches official type structure
Quick Start
import Koa from 'koa';
import Router from '@koa/router';
const app = new Koa();
const router = new Router();
// Define routes
router.get('/', (ctx, next) => {
ctx.body = 'Hello World!';
});
router.get('/users/:id', (ctx, next) => {
ctx.body = { id: ctx.params.id };
});
// Apply router middleware
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000);API Documentation
Router Constructor
new Router([options])
Create a new router instance.
Options:
| Option | Type | Description |
| ----------- | ------------------------------ | ----------------------------------------- |
| prefix | string | Prefix all routes with this path |
| exclusive | boolean | Only run the most specific matching route |
| host | string \| string[] \| RegExp | Match routes only for this hostname(s) |
| methods | string[] | Custom HTTP methods to support |
| sensitive | boolean | Enable case-sensitive routing |
| strict | boolean | Require trailing slashes |
Example:
const router = new Router({
prefix: '/api',
exclusive: true,
host: 'example.com'
});HTTP Methods
Router provides methods for all standard HTTP verbs:
router.get(path, ...middleware)router.post(path, ...middleware)router.put(path, ...middleware)router.patch(path, ...middleware)router.delete(path, ...middleware)orrouter.del(path, ...middleware)router.head(path, ...middleware)router.options(path, ...middleware)router.connect(path, ...middleware)- CONNECT methodrouter.trace(path, ...middleware)- TRACE methodrouter.all(path, ...middleware)- Match any HTTP method
Note: All standard HTTP methods (as defined by Node.js http.METHODS) are automatically available as router methods. The methods option in the constructor can be used to limit which methods the router responds to, but you cannot use truly custom HTTP methods beyond the standard set.
Basic Example:
router
.get('/users', getUsers)
.post('/users', createUser)
.put('/users/:id', updateUser)
.delete('/users/:id', deleteUser)
.all('/users/:id', logAccess); // Runs for any methodUsing Less Common HTTP Methods:
All standard HTTP methods from Node.js are automatically available. Here's an example using PATCH and PURGE:
const router = new Router();
// PATCH method (standard HTTP method for partial updates)
router.patch('/users/:id', async (ctx) => {
// Partial update
ctx.body = { message: 'User partially updated' };
});
// PURGE method (standard HTTP method, commonly used for cache invalidation)
router.purge('/cache/:key', async (ctx) => {
// Clear cache
await clearCache(ctx.params.key);
ctx.body = { message: 'Cache cleared' };
});
// COPY method (standard HTTP method)
router.copy('/files/:source', async (ctx) => {
await copyFile(ctx.params.source, ctx.request.body.destination);
ctx.body = { message: 'File copied' };
});
// Limiting which methods the router responds to
const apiRouter = new Router({
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] // Only these methods
});
apiRouter.get('/users', getUsers);
apiRouter.post('/users', createUser);
// router.purge() won't work here because PURGE is not in the methods arrayUsing Less Common HTTP Methods:
All standard HTTP methods from Node.js are automatically available. Here's an example using PATCH and PURGE:
const router = new Router();
// PATCH method (standard HTTP method for partial updates)
router.patch('/users/:id', async (ctx) => {
// Partial update
ctx.body = { message: 'User partially updated' };
});
// PURGE method (standard HTTP method, commonly used for cache invalidation)
router.purge('/cache/:key', async (ctx) => {
// Clear cache
await clearCache(ctx.params.key);
ctx.body = { message: 'Cache cleared' };
});
// COPY method (standard HTTP method)
router.copy('/files/:source', async (ctx) => {
await copyFile(ctx.params.source, ctx.request.body.destination);
ctx.body = { message: 'File copied' };
});
// Limiting which methods the router responds to
const apiRouter = new Router({
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] // Only these methods
});
apiRouter.get('/users', getUsers);
apiRouter.post('/users', createUser);
// router.purge() won't work here because PURGE is not in the methods arrayNote: HEAD requests are automatically supported for all GET routes. When you define a GET route, HEAD requests will execute the same handler and return the same headers but with an empty body.
Named Routes
Routes can be named for URL generation:
router.get('user', '/users/:id', (ctx) => {
ctx.body = { id: ctx.params.id };
});
// Generate URL
router.url('user', 3);
// => "/users/3"
router.url('user', { id: 3 });
// => "/users/3"
// With query parameters
router.url('user', { id: 3 }, { query: { limit: 10 } });
// => "/users/3?limit=10"
// In middleware
router.use((ctx, next) => {
ctx.redirect(ctx.router.url('user', 1));
});Multiple Middleware
Chain multiple middleware functions for a single route:
router.get(
'/users/:id',
async (ctx, next) => {
// Load user from database
ctx.state.user = await User.findById(ctx.params.id);
return next();
},
async (ctx, next) => {
// Check permissions
if (!ctx.state.user) {
ctx.throw(404, 'User not found');
}
return next();
},
(ctx) => {
// Send response
ctx.body = ctx.state.user;
}
);Nested Routers
Mount routers within routers:
const usersRouter = new Router();
usersRouter.get('/', getUsers);
usersRouter.get('/:id', getUser);
const postsRouter = new Router();
postsRouter.get('/', getPosts);
postsRouter.get('/:id', getPost);
const apiRouter = new Router({ prefix: '/api' });
apiRouter.use('/users', usersRouter.routes());
apiRouter.use('/posts', postsRouter.routes());
app.use(apiRouter.routes());Note: Parameters from parent routes are properly propagated to nested router middleware and handlers.
Router Prefixes
Set a prefix for all routes in a router:
Option 1: In constructor
const router = new Router({ prefix: '/api' });
router.get('/users', handler); // Responds to /api/usersOption 2: Using .prefix()
const router = new Router();
router.prefix('/api');
router.get('/users', handler); // Responds to /api/usersWith parameters:
const router = new Router({ prefix: '/api/v:version' });
router.get('/users', (ctx) => {
ctx.body = {
version: ctx.params.version,
users: []
};
});
// Responds to /api/v1/users, /api/v2/users, etc.Note: Middleware now correctly executes when the prefix contains parameters.
URL Parameters
Named parameters are captured and available at ctx.params:
router.get('/:category/:title', (ctx) => {
console.log(ctx.params);
// => { category: 'programming', title: 'how-to-node' }
ctx.body = {
category: ctx.params.category,
title: ctx.params.title
};
});Optional parameters:
router.get('/user{/:id}?', (ctx) => {
// Matches both /user and /user/123
ctx.body = { id: ctx.params.id || 'all' };
});Wildcard parameters:
router.get('/files/{/*path}', (ctx) => {
// Matches /files/a/b/c.txt
ctx.body = { path: ctx.params.path }; // => a/b/c.txt
});Note: Custom regex patterns in parameters (:param(regex)) are no longer supported in v14+ due to path-to-regexp v8. Use validation in handlers or middleware instead.
router.routes()
Returns router middleware which dispatches matched routes.
app.use(router.routes());router.use()
Use middleware, if and only if, a route is matched.
Signature:
router.use([path], ...middleware);Examples:
// Run for all matched routes
router.use(session());
// Run only for specific path
router.use('/admin', requireAuth());
// Run for multiple paths
router.use(['/admin', '/dashboard'], requireAuth());
// Run for RegExp paths
router.use(/^\/api\//, apiAuth());
// Mount nested routers
const nestedRouter = new Router();
router.use('/nested', nestedRouter.routes());Note: Middleware path boundaries are correctly enforced. Middleware scoped to /api will only run for routes matching /api/*, not for unrelated routes.
router.prefix()
Set the path prefix for a Router instance after initialization.
const router = new Router();
router.get('/', handler); // Responds to /
router.prefix('/api');
router.get('/', handler); // Now responds to /apirouter.allowedMethods()
Returns middleware for responding to OPTIONS requests with allowed methods,
and 405 Method Not Allowed / 501 Not Implemented responses.
Options:
| Option | Type | Description |
| ------------------ | ---------- | ---------------------------------------- |
| throw | boolean | Throw errors instead of setting response |
| notImplemented | function | Custom function for 501 errors |
| methodNotAllowed | function | Custom function for 405 errors |
Example:
app.use(router.routes());
app.use(router.allowedMethods());With custom error handling:
app.use(
router.allowedMethods({
throw: true,
notImplemented: () => new Error('Not Implemented'),
methodNotAllowed: () => new Error('Method Not Allowed')
})
);router.redirect()
Redirect source to destination URL with optional status code.
router.redirect('/login', 'sign-in', 301);
router.redirect('/old-path', '/new-path');
// Redirect to named route
router.get('home', '/', handler);
router.redirect('/index', 'home');router.route()
Lookup a route by name.
const layer = router.route('user');
if (layer) {
console.log(layer.path); // => /users/:id
}router.url()
Generate URL from route name and parameters.
router.get('user', '/users/:id', handler);
router.url('user', 3);
// => "/users/3"
router.url('user', { id: 3 });
// => "/users/3"
router.url('user', { id: 3 }, { query: { limit: 1 } });
// => "/users/3?limit=1"
router.url('user', { id: 3 }, { query: 'limit=1' });
// => "/users/3?limit=1"In middleware:
router.use((ctx, next) => {
// Access router instance via ctx.router
const userUrl = ctx.router.url('user', ctx.state.userId);
ctx.redirect(userUrl);
return next();
});router.param()
Run middleware for named route parameters.
Signature:
router.param(param: string, middleware: RouterParameterMiddleware): RouterTypeScript Example:
import type { RouterParameterMiddleware } from '@koa/router';
import type { Next } from 'koa';
router.param('user', (async (id: string, ctx: RouterContext, next: Next) => {
ctx.state.user = await User.findById(id);
if (!ctx.state.user) {
ctx.throw(404, 'User not found');
}
return next();
}) as RouterParameterMiddleware);
router.get('/users/:user', (ctx: RouterContext) => {
// ctx.state.user is already loaded and typed
ctx.body = ctx.state.user;
});
router.get('/users/:user/friends', (ctx: RouterContext) => {
// ctx.state.user is available here too
return ctx.state.user.getFriends();
});JavaScript Example:
router
.param('user', async (id, ctx, next) => {
ctx.state.user = await User.findById(id);
if (!ctx.state.user) {
ctx.throw(404, 'User not found');
}
return next();
})
.get('/users/:user', (ctx) => {
// ctx.state.user is already loaded
ctx.body = ctx.state.user;
})
.get('/users/:user/friends', (ctx) => {
// ctx.state.user is available here too
return ctx.state.user.getFriends();
});Multiple param handlers:
You can register multiple param handlers for the same parameter. All handlers will be called in order, and each handler is executed exactly once per request (even if multiple routes match):
router
.param('id', validateIdFormat)
.param('id', checkIdExists)
.param('id', checkPermissions)
.get('/resource/:id', handler);
// All three param handlers run once per requestRouter.url() (static)
Generate URL from path pattern and parameters (static method).
const url = Router.url('/users/:id', { id: 1 });
// => "/users/1"
const url = Router.url('/users/:id', { id: 1, name: 'John' });
// => "/users/1"Advanced Features
Host Matching
Match routes only for specific hostnames:
// Exact match with single host
const routerA = new Router({
host: 'example.com'
});
// Match multiple hosts with array
const routerB = new Router({
host: ['some-domain.com', 'www.some-domain.com', 'some.other-domain.com']
});
// Match patterns with RegExp
const routerC = new Router({
host: /^(.*\.)?example\.com$/ // Match all subdomains
});Host Matching Options:
string- Exact match (case-sensitive)string[]- Matches if the request host equals any string in the arrayRegExp- Pattern match using regular expressionundefined- Matches all hosts (default)
Regular Expressions
Use RegExp for flexible path matching:
Full RegExp routes:
router.get(/^\/users\/(\d+)$/, (ctx) => {
const id = ctx.params[0]; // First capture group
ctx.body = { id };
});RegExp in router.use():
router.use(/^\/api\//, apiMiddleware);
router.use(/^\/admin\//, adminAuth);Parameter Validation
Validate parameters using middleware or handlers:
Option 1: In Handler
router.get('/user/:id', (ctx) => {
if (!/^\d+$/.test(ctx.params.id)) {
ctx.throw(400, 'Invalid ID format');
}
ctx.body = { id: parseInt(ctx.params.id, 10) };
});Option 2: Middleware
function validateUUID(paramName) {
const uuidRegex =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
return async (ctx, next) => {
if (!uuidRegex.test(ctx.params[paramName])) {
ctx.throw(400, `Invalid ${paramName} format`);
}
await next();
};
}
router.get('/user/:id', validateUUID('id'), handler);Option 3: router.param()
router.param('id', (value, ctx, next) => {
if (!/^\d+$/.test(value)) {
ctx.throw(400, 'Invalid ID');
}
ctx.params.id = parseInt(value, 10); // Convert to number
return next();
});
router.get('/user/:id', handler);
router.get('/post/:id', handler);
// Both routes validate :id parameterCatch-All Routes
Create a catch-all route that only runs when no other routes match:
router.get('/users', handler1);
router.get('/posts', handler2);
// Catch-all for unmatched routes
router.all('{/*rest}', (ctx) => {
if (!ctx.matched || ctx.matched.length === 0) {
ctx.status = 404;
ctx.body = { error: 'Not Found' };
}
});Array of Paths
Register multiple paths with the same middleware:
router.get(['/users', '/people'], handler);
// Responds to both /users and /people404 Handling
Implement custom 404 handling:
app.use(router.routes());
// 404 middleware - runs after router
app.use((ctx) => {
if (!ctx.matched || ctx.matched.length === 0) {
ctx.status = 404;
ctx.body = {
error: 'Not Found',
path: ctx.path
};
}
});Best Practices
1. Use Middleware Composition
// ✅ Good: Compose reusable middleware
const requireAuth = () => async (ctx, next) => {
if (!ctx.state.user) ctx.throw(401);
await next();
};
const requireAdmin = () => async (ctx, next) => {
if (!ctx.state.user.isAdmin) ctx.throw(403);
await next();
};
router.get('/admin', requireAuth(), requireAdmin(), adminHandler);2. Organize Routes by Resource
// ✅ Good: Group related routes
const usersRouter = new Router({ prefix: '/users' });
usersRouter.get('/', listUsers);
usersRouter.post('/', createUser);
usersRouter.get('/:id', getUser);
usersRouter.put('/:id', updateUser);
usersRouter.delete('/:id', deleteUser);
app.use(usersRouter.routes());3. Use Named Routes
// ✅ Good: Name important routes
router.get('home', '/', homeHandler);
router.get('user-profile', '/users/:id', profileHandler);
// Easy to generate URLs
ctx.redirect(ctx.router.url('home'));
ctx.redirect(ctx.router.url('user-profile', ctx.state.user.id));4. Validate Early
// ✅ Good: Validate at the route level
router
.param('id', validateId)
.get('/users/:id', getUser)
.put('/users/:id', updateUser)
.delete('/users/:id', deleteUser);
// Validation runs once for all routes5. Handle Errors Consistently
// ✅ Good: Centralized error handling
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
error: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
};
}
});
app.use(router.routes());
app.use(router.allowedMethods({ throw: true }));6. Access Router Context Properties
The router adds useful properties to the Koa context:
router.get('/users/:id', (ctx: RouterContext) => {
// URL parameters (fully typed)
const id = ctx.params.id; // string
// Router instance
const router = ctx.router;
// Matched route path
const routePath = ctx.routerPath; // => '/users/:id'
// Matched route name (if named)
const routeName = ctx.routerName; // => 'user' (if named)
// All matched layers
const matched = ctx.matched; // Array of Layer objects
// Captured values from RegExp routes
const captures = ctx.captures; // string[] | undefined
// Generate URLs
const url = ctx.router.url('user', id);
ctx.body = { id, routePath, routeName, url };
});7. Type-Safe Context Extensions
Extend the router context with custom properties:
import Router, { RouterContext } from '@koa/router';
import type { Next } from 'koa';
interface UserState {
user?: { id: string; email: string };
}
interface CustomContext {
requestId: string;
startTime: number;
}
const router = new Router<UserState, CustomContext>();
// Middleware that adds to context
router.use(async (ctx: RouterContext<UserState, CustomContext>, next: Next) => {
ctx.requestId = crypto.randomUUID();
ctx.startTime = Date.now();
await next();
});
router.get(
'/users/:id',
async (ctx: RouterContext<UserState, CustomContext>) => {
// All properties are fully typed
ctx.body = {
user: ctx.state.user,
requestId: ctx.requestId,
duration: Date.now() - ctx.startTime
};
}
);Recipes
Common patterns and recipes for building real-world applications with @koa/router.
See the recipes directory for complete TypeScript examples:
- Nested Routes - Production-ready nested router patterns with multiple levels (3-4 levels deep), parameter propagation, and real-world examples
- RESTful API Structure - Organize your API with nested routers
- Authentication & Authorization - JWT-based authentication with middleware
- Request Validation - Validate request data with middleware
- Parameter Validation - Validate and transform parameters using router.param()
- API Versioning - Implement API versioning with multiple routers
- Error Handling - Centralized error handling with custom error classes
- Pagination - Implement pagination for list endpoints
- Health Checks - Add health check endpoints for monitoring
- TypeScript Recipe - Full TypeScript example with types and type safety
Each recipe file contains complete, runnable TypeScript code that you can copy and adapt to your needs.
Performance
@koa/router is designed for high performance:
- Fast path matching with path-to-regexp v8
- Efficient RegExp compilation and caching
- Minimal overhead - zero runtime type checking
- Optimized middleware execution with koa-compose
Benchmarks:
# Run benchmarks
yarn benchmark
# Run all benchmark scenarios
yarn benchmark:allTesting
@koa/router uses Node.js native test runner:
# Run all tests (core + recipes)
yarn test:all
# Run core tests only
yarn test:core
# Run recipe tests only
yarn test:recipes
# Run tests with coverage
yarn test:coverage
# Type check
yarn ts:check
# Format code with Prettier
yarn format
# Check code formatting
yarn format:check
# Lint code
yarn lintExample test:
import { describe, it } from 'node:test';
import assert from 'node:assert';
import Koa from 'koa';
import Router from '@koa/router';
import request from 'supertest';
describe('Router', () => {
it('should route GET requests', async () => {
const app = new Koa();
const router = new Router();
router.get('/users', (ctx) => {
ctx.body = { users: [] };
});
app.use(router.routes());
const res = await request(app.callback()).get('/users').expect(200);
assert.deepStrictEqual(res.body, { users: [] });
});
});Migration Guides
Breaking Changes:
- Custom regex patterns in parameters (
:param(regex)) are no longer supported due to path-to-regexp v8. Use validation in handlers or middleware instead. - Node.js >= 20 is required.
- TypeScript types are now included in the package (no need for
@types/@koa/router).
Upgrading:
- Update Node.js to >= 20
- Replace custom regex parameters with validation middleware
- Remove
@types/@koa/routerif installed (types are now included) - Update any code using deprecated features
Backward Compatibility:
The code is mostly backward compatible. If you notice any issues when upgrading, please don't hesitate to open an issue and let us know!
Contributing
Contributions are welcome!
Development Setup
# Clone repository
git clone https://github.com/koajs/router.git
cd router
# Install dependencies (using yarn)
yarn install
# Run tests
yarn test:all
# Run tests with coverage
yarn test:coverage
# Format code
yarn format
# Check formatting
yarn format:check
# Lint code
yarn lint
# Build TypeScript
yarn build
# Type check
yarn ts:checkContributors
| Name | | ---------------- | | Alex Mingoia | | @koajs | | Imed Jaberi |
License
MIT © Koa.js
