Advanced Routing
Router Merging
Routers can be merged, which brings one router’s routes into another, with a prefix. This is incredibly useful for larger apps, for example when you have multiple versions of an API.
import {v1} from './routers/v1.ts';
import {v2} from './routers/v2.ts';
export const api = router.merge('/v1', v1).merge('/v2', v2);All type information, route names, and correct prefixes are preserved during merging.
Router Parameters
The .params() method lets you declare what URL parameters a router expects when it gets merged. Pass the param names as a type argument, like .params<'userId'>() — no runtime validation needed, params are always strings extracted from the URL. This only matters when the router is merged somewhere that provides those parameters.
Basic Parameters
// Declare that this router needs a userId parameter
const userRouter = router.params<'userId'>().get('/', async ({params}) => {
// params.userId is guaranteed to exist and is a string
return {id: params.userId};
});
// This works - we're mounting at a path that provides userId
const app = router.merge('/users/:userId', userRouter);
// This would be a type error - we're not providing userId
const badApp = router.merge('/users', userRouter);Nested Parameters
Parameters become especially powerful with deeply nested routers:
// This router needs both userId and postId
const commentsRouter = router.params<'userId' | 'postId'>().pipe(async (ctx, params) => {
const [user, post] = await Promise.all([db.users.findById(params.userId), db.posts.findById(params.postId)]);
if (!user || !post) {
throw new KaitoError(404, 'Not found');
}
return {
...ctx,
user,
post,
};
});
// This router provides postId and forwards userId
const postsRouter = router.params<'userId'>().merge('/posts/:postId/comments', commentsRouter);
// Finally, we provide userId at the top level
const app = router.merge('/users/:userId', postsRouter);You can only call .params() once on a router. Multiple calls will result in a type error to prevent breaking
existing routes.
Pipes
Kaito uses .pipe() instead of traditional middleware to transform context before routes run. Pipes are type-safe, composable, and chainable. See the dedicated Pipes page for full documentation, including chaining, plugins, reusable routers, and advanced generic patterns.