Modern TypeScript Router middleware for Koa. Maintained by Forward Email and Lad.

Table of Contents
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/router
yarn:
yarn add @koa/router
Requirements:
- 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
Types are automatically inferred - no explicit type annotations needed:
import Router from '@koa/router';
const router = new Router();
router.get('/:id', (ctx, next) => {
const id = ctx.params.id;
ctx.request.params.id;
ctx.body = { id };
return next();
});
router.use((ctx, next) => {
ctx.state.startTime = Date.now();
return next();
});
Explicit Types (Optional)
For cases where you need explicit types:
import Router, { RouterContext } from '@koa/router';
import type { Next } from 'koa';
router.get('/:id', (ctx: RouterContext, next: Next) => {
const id = ctx.params.id;
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';
interface AppState {
user?: {
id: string;
email: string;
};
}
interface AppContext {
requestId: string;
}
const router = new Router<AppState, AppContext>();
router.get(
'/profile',
(ctx: RouterContext<AppState, AppContext>, next: Next) => {
if (ctx.state.user) {
ctx.body = {
user: ctx.state.user,
requestId: ctx.requestId
};
}
}
);
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[];
}
router.get<UserState, UserContext>(
'/users/:id',
async (ctx: RouterContext<UserState, UserContext>) => {
ctx.body = {
user: ctx.state.user,
permissions: ctx.permissions
};
}
);
Parameter Middleware Types
import type { RouterParameterMiddleware } from '@koa/router';
import type { Next } from 'koa';
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';
type MyRouter = Router<AppState, AppContext>;
type MyContext = RouterContext<AppState, AppContext, BodyType>;
type MyMiddleware = RouterMiddleware<AppState, AppContext, BodyType>;
type MyParamMiddleware = RouterParameterMiddleware<
AppState,
AppContext,
BodyType
>;
Type Safety Features
- ✅ Full type inference -
ctx and next are inferred automatically in route handlers
- ✅ Full generic support -
Router<StateT, ContextT> for custom state and context types
- ✅ Type-safe parameters -
ctx.params is fully typed and always defined
- ✅ Type-safe state -
ctx.state respects your state type
- ✅ Type-safe middleware - Middleware functions are fully typed
- ✅ Type-safe HTTP methods - Methods support generic type extensions
- ✅ Custom HTTP method inference - Use
as const with methods option for typed custom methods
- ✅ 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();
router.get('/', (ctx, next) => {
ctx.body = 'Hello World!';
});
router.get('/users/:id', (ctx, next) => {
ctx.body = { id: ctx.params.id };
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000);
API Documentation
Router Constructor
new Router([options])
Create a new router instance.
Options:
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) or router.del(path, ...middleware)
router.head(path, ...middleware)
router.options(path, ...middleware)
router.connect(path, ...middleware) - CONNECT method
router.trace(path, ...middleware) - TRACE method
router.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);
Using 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();
router.patch('/users/:id', async (ctx) => {
ctx.body = { message: 'User partially updated' };
});
router.purge('/cache/:key', async (ctx) => {
await clearCache(ctx.params.key);
ctx.body = { message: 'Cache cleared' };
});
router.copy('/files/:source', async (ctx) => {
await copyFile(ctx.params.source, ctx.request.body.destination);
ctx.body = { message: 'File copied' };
});
const apiRouter = new Router({
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
});
apiRouter.get('/users', getUsers);
apiRouter.post('/users', createUser);
Note: 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 };
});
router.url('user', 3);
router.url('user', { id: 3 });
router.url('user', { id: 3 }, { query: { limit: 10 } });
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) => {
ctx.state.user = await User.findById(ctx.params.id);
return next();
},
async (ctx, next) => {
if (!ctx.state.user) {
ctx.throw(404, 'User not found');
}
return next();
},
(ctx) => {
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);
Option 2: Using .prefix()
const router = new Router();
router.prefix('/api');
router.get('/users', handler);
With parameters:
const router = new Router({ prefix: '/api/v:version' });
router.get('/users', (ctx) => {
ctx.body = {
version: ctx.params.version,
users: []
};
});
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);
ctx.body = {
category: ctx.params.category,
title: ctx.params.title
};
});
Optional parameters:
router.get('/user{/:id}?', (ctx) => {
ctx.body = { id: ctx.params.id || 'all' };
});
Wildcard parameters:
router.get('/files/{/*path}', (ctx) => {
ctx.body = { path: ctx.params.path };
});
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:
router.use(session());
router.use('/admin', requireAuth());
router.use(['/admin', '/dashboard'], requireAuth());
router.use(/^\/api\//, apiAuth());
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);
router.prefix('/api');
router.get('/', handler);
router.allowedMethods()
Returns middleware for responding to OPTIONS requests with allowed methods,
and 405 Method Not Allowed / 501 Not Implemented responses.
Options:
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');
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);
}
router.url()
Generate URL from route name and parameters.
router.get('user', '/users/:id', handler);
router.url('user', 3);
router.url('user', { id: 3 });
router.url('user', { id: 3 }, { query: { limit: 1 } });
router.url('user', { id: 3 }, { query: 'limit=1' });
In middleware:
router.use((ctx, next) => {
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): Router
TypeScript 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.body = ctx.state.user;
});
router.get('/users/:user/friends', (ctx: RouterContext) => {
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.body = ctx.state.user;
})
.get('/users/:user/friends', (ctx) => {
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);
Router.url() (static)
Generate URL from path pattern and parameters (static method).
const url = Router.url('/users/:id', { id: 1 });
const url = Router.url('/users/:id', { id: 1, name: 'John' });
Advanced Features
Host Matching
Match routes only for specific hostnames:
const routerA = new Router({
host: 'example.com'
});
const routerB = new Router({
host: ['some-domain.com', 'www.some-domain.com', 'some.other-domain.com']
});
const routerC = new Router({
host: /^(.*\.)?example\.com$/
});
Host Matching Options:
string - Exact match (case-sensitive)
string[] - Matches if the request host equals any string in the array
RegExp - Pattern match using regular expression
undefined - 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];
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);
return next();
});
router.get('/user/:id', handler);
router.get('/post/:id', handler);
Catch-All Routes
Create a catch-all route that only runs when no other routes match:
router.get('/users', handler1);
router.get('/posts', handler2);
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);
404 Handling
Implement custom 404 handling:
app.use(router.routes());
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
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
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
router.get('home', '/', homeHandler);
router.get('user-profile', '/users/:id', profileHandler);
ctx.redirect(ctx.router.url('home'));
ctx.redirect(ctx.router.url('user-profile', ctx.state.user.id));
4. Validate Early
router
.param('id', validateId)
.get('/users/:id', getUser)
.put('/users/:id', updateUser)
.delete('/users/:id', deleteUser);
5. Handle Errors Consistently
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) => {
const id = ctx.params.id;
const router = ctx.router;
const routePath = ctx.routerPath;
const routeName = ctx.routerName;
const matched = ctx.matched;
const captures = ctx.captures;
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>();
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>) => {
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:
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:
yarn benchmark
yarn benchmark:all
Testing
@koa/router uses Node.js native test runner:
yarn test:all
yarn test:core
yarn test:recipes
yarn test:coverage
yarn ts:check
yarn format
yarn format:check
yarn lint
Example 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
For detailed migration information, see FULL_MIGRATION_TO_V15+.md.
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/router if 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
git clone https://github.com/koajs/router.git
cd router
yarn install
yarn test:all
yarn test:coverage
yarn format
yarn format:check
yarn lint
yarn build
yarn ts:check
Contributors
| Alex Mingoia |
| @koajs |
| Imed Jaberi |
License
MIT © Koa.js