@koa/router
Version:
Router middleware for koa. Maintained by Forward Email and Lad.
1,174 lines (875 loc) • 28.8 kB
Markdown
# [@koa/router](https://github.com/koajs/router)
> Modern TypeScript Router middleware for [Koa](https://github.com/koajs/koa). Maintained by [Forward Email][forward-email] and [Lad][].
[](https://github.com/koajs/router/actions/workflows/ci.yml)
[](https://github.com/prettier/prettier)
[](https://lass.js.org)
[](LICENSE)
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [TypeScript Support](#typescript-support)
- [Quick Start](#quick-start)
- [API Documentation](#api-documentation)
- [Advanced Features](#advanced-features)
- [Best Practices](#best-practices)
- [Recipes](#recipes)
- [Performance](#performance)
- [Testing](#testing)
- [Migration Guides](#migration-guides)
- [Contributing](#contributing)
- [License](#license)
- [Contributors](#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:**
```bash
npm install @koa/router
```
**yarn:**
```bash
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:
```typescript
import Router from '@koa/router';
const router = new Router();
// ctx and next are automatically inferred!
router.get('/:id', (ctx, next) => {
const id = ctx.params.id; // ✅ Inferred as string
ctx.request.params.id; // ✅ Also available
ctx.body = { id }; // ✅ Works
return next(); // ✅ Works
});
// Also works for router.use()
router.use((ctx, next) => {
ctx.state.startTime = Date.now();
return next();
});
```
### Explicit Types (Optional)
For cases where you need explicit types:
```typescript
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:
```typescript
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:
```typescript
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
```typescript
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
```typescript
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 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
```javascript
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:**
```javascript
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:**
```javascript
router
.get('/users', getUsers)
.post('/users', createUser)
.put('/users/:id', updateUser)
.delete('/users/:id', deleteUser)
.all('/users/:id', logAccess); // Runs for any method
```
**Using Less Common HTTP Methods:**
All standard HTTP methods from Node.js are automatically available. Here's an example using `PATCH` and `PURGE`:
```javascript
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 array
```
**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:
```javascript
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:
```javascript
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:
```javascript
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**
```javascript
const router = new Router({ prefix: '/api' });
router.get('/users', handler); // Responds to /api/users
```
**Option 2: Using .prefix()**
```javascript
const router = new Router();
router.prefix('/api');
router.get('/users', handler); // Responds to /api/users
```
**With parameters:**
```javascript
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`:
```javascript
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:**
```javascript
router.get('/user{/:id}?', (ctx) => {
// Matches both /user and /user/123
ctx.body = { id: ctx.params.id || 'all' };
});
```
**Wildcard parameters:**
```javascript
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.
```javascript
app.use(router.routes());
```
### router.use()
Use middleware, **if and only if**, a route is matched.
**Signature:**
```javascript
router.use([path], ...middleware);
```
**Examples:**
```javascript
// 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.
```javascript
const router = new Router();
router.get('/', handler); // Responds to /
router.prefix('/api');
router.get('/', handler); // Now responds to /api
```
### router.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:**
```javascript
app.use(router.routes());
app.use(router.allowedMethods());
```
**With custom error handling:**
```javascript
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.
```javascript
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.
```javascript
const layer = router.route('user');
if (layer) {
console.log(layer.path); // => /users/:id
}
```
### router.url()
Generate URL from route name and parameters.
```javascript
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:**
```javascript
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:**
```typescript
router.param(param: string, middleware: RouterParameterMiddleware): Router
```
**TypeScript Example:**
```typescript
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:**
```javascript
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):
```javascript
router
.param('id', validateIdFormat)
.param('id', checkIdExists)
.param('id', checkPermissions)
.get('/resource/:id', handler);
// All three param handlers run once per request
```
### Router.url() (static)
Generate URL from path pattern and parameters (static method).
```javascript
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:
```javascript
// 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 array
- `RegExp` - Pattern match using regular expression
- `undefined` - Matches all hosts (default)
### Regular Expressions
Use RegExp for flexible path matching:
**Full RegExp routes:**
```javascript
router.get(/^\/users\/(\d+)$/, (ctx) => {
const id = ctx.params[0]; // First capture group
ctx.body = { id };
});
```
**RegExp in router.use():**
```javascript
router.use(/^\/api\//, apiMiddleware);
router.use(/^\/admin\//, adminAuth);
```
### Parameter Validation
Validate parameters using middleware or handlers:
**Option 1: In Handler**
```javascript
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**
```javascript
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()**
```javascript
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 parameter
```
### Catch-All Routes
Create a catch-all route that only runs when no other routes match:
```javascript
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:
```javascript
router.get(['/users', '/people'], handler);
// Responds to both /users and /people
```
### 404 Handling
Implement custom 404 handling:
```javascript
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
```javascript
// ✅ 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
```javascript
// ✅ 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
```javascript
// ✅ 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
```javascript
// ✅ 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 routes
```
### 5. Handle Errors Consistently
```javascript
// ✅ 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:
```typescript
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:
```typescript
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](./recipes/) for complete TypeScript examples:
- **[Nested Routes](./recipes/nested-routes/)** - Production-ready nested router patterns with multiple levels (3-4 levels deep), parameter propagation, and real-world examples
- **[RESTful API Structure](./recipes/restful-api-structure/)** - Organize your API with nested routers
- **[Authentication & Authorization](./recipes/authentication-authorization/)** - JWT-based authentication with middleware
- **[Request Validation](./recipes/request-validation/)** - Validate request data with middleware
- **[Parameter Validation](./recipes/parameter-validation/)** - Validate and transform parameters using router.param()
- **[API Versioning](./recipes/api-versioning/)** - Implement API versioning with multiple routers
- **[Error Handling](./recipes/error-handling/)** - Centralized error handling with custom error classes
- **[Pagination](./recipes/pagination/)** - Implement pagination for list endpoints
- **[Health Checks](./recipes/health-checks/)** - Add health check endpoints for monitoring
- **[TypeScript Recipe](./recipes/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:**
```bash
# Run benchmarks
yarn benchmark
# Run all benchmark scenarios
yarn benchmark:all
```
## Testing
@koa/router uses Node.js native test runner:
```bash
# 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 lint
```
**Example test:**
```javascript
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](./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:**
1. Update Node.js to >= 20
2. Replace custom regex parameters with validation middleware
3. Remove `@types/@koa/router` if installed (types are now included)
4. 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](https://github.com/koajs/router/issues) and let us know!
## Contributing
Contributions are welcome!
### Development Setup
```bash
# 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:check
```
## Contributors
| Name |
| ---------------- |
| **Alex Mingoia** |
| **@koajs** |
| **Imed Jaberi** |
## License
[MIT](LICENSE) © Koa.js
---
[forward-email]: https://forwardemail.net
[lad]: https://lad.js.org
[npm]: https://www.npmjs.com