Router Composition
As your API grows, it becomes important to split routes across multiple files and compose them
together. feTS supports this through the .use() method on the router, allowing you to merge
sub-routers into a parent router.
Server Context
Handlers receive a second context argument typed from createRouter<TServerContext>(). Use it to
pass platform-specific values (for example the underlying Node/uWebSockets request) or
request-scoped data from plugins.
import { createRouter, Response } from 'fets'
interface AppContext {
userId?: string
}
const router = createRouter<AppContext>().route({
method: 'GET',
path: '/me',
handler(_request, context) {
return Response.json({ userId: context.userId ?? null })
}
})
// `fetch(url, init?, ...ctx)` — context comes after the optional RequestInit
// `handle(request, context)` — context is the second argument
await router.fetch('http://localhost/me', {}, { userId: '42' })Platform integrations often type ServerContext with the native request/response objects — see
uWebSockets for an example.
Basic Usage
You can merge a sub-router into a parent router by calling .use() with the sub-router as the
argument. The sub-router’s routes will be registered at their original paths.
import { createRouter, Response } from 'fets'
export const usersRouter = createRouter()
.route({
path: '/users',
method: 'GET',
schemas: {
responses: {
200: {
type: 'object',
properties: {
users: { type: 'array', items: { type: 'string' } }
},
required: ['users'],
additionalProperties: false
}
}
},
handler: () => Response.json({ users: ['alice', 'bob'] })
})
.route({
path: '/users/:id',
method: 'GET',
handler: request => Response.json({ id: request.params.id })
})import { createServer } from 'node:http'
import { createRouter } from 'fets'
import { postsRouter } from './posts-router'
import { usersRouter } from './users-router'
const app = createRouter().use(usersRouter).use(postsRouter)
createServer(app).listen(3000)Using a Prefix
You can mount a sub-router under a path prefix by passing the prefix as the first argument to
.use(). All routes from the sub-router will have the prefix prepended to their paths.
import { createRouter, Response } from 'fets'
export const usersRouter = createRouter()
.route({
path: '/',
method: 'GET',
handler: () => Response.json({ users: ['alice', 'bob'] })
})
.route({
path: '/:id',
method: 'GET',
handler: request => Response.json({ id: request.params.id })
})import { createServer } from 'node:http'
import { createRouter } from 'fets'
import { usersRouter } from './users-router'
const app = createRouter().use('/users', usersRouter)
// GET /users → usersRouter's '/' handler
// GET /users/:id → usersRouter's '/:id' handler
createServer(app).listen(3000)Sub-Router Base Path
If a sub-router was created with a base option, that base path is also taken into account when
merging:
import { createRouter, Response } from 'fets'
export const usersRouter = createRouter({ base: '/users' }).route({
path: '/:id',
method: 'GET',
handler: request => Response.json({ id: request.params.id })
})import { createRouter } from 'fets'
import { usersRouter } from './users-router'
const app = createRouter().use(usersRouter)
// GET /users/:id → usersRouter's '/:id' handlerComposing Multiple Levels
Routers can be composed transitively — a merged router can itself be merged into another:
import { createRouter, Response } from 'fets'
// Deepest level
const itemsRouter = createRouter().route({
path: '/:id',
method: 'GET',
handler: request => Response.json({ id: request.params.id })
})
// Mid level
const apiRouter = createRouter().use('/items', itemsRouter)
// /items/:id is now registered
// Top level
const app = createRouter().use('/api', apiRouter)
// GET /api/items/:id → itemsRouter's '/:id' handlerFull Example
Here is a complete example showing how to organise a larger API using router composition:
import { createRouter, Response } from 'fets'
export const usersRouter = createRouter().route({
path: '/users',
method: 'GET',
schemas: {
responses: {
200: {
type: 'object',
properties: {
users: { type: 'array', items: { type: 'string' } }
},
required: ['users'],
additionalProperties: false
}
}
},
handler: () => Response.json({ users: ['alice', 'bob'] })
})import { createRouter, Response } from 'fets'
export const postsRouter = createRouter().route({
path: '/posts',
method: 'GET',
schemas: {
responses: {
200: {
type: 'object',
properties: {
posts: { type: 'array', items: { type: 'string' } }
},
required: ['posts'],
additionalProperties: false
}
}
},
handler: () => Response.json({ posts: ['hello world', 'feTS is great'] })
})import { createServer } from 'node:http'
import { createRouter } from 'fets'
import { postsRouter } from './posts-router'
import { usersRouter } from './users-router'
const app = createRouter().use(usersRouter).use(postsRouter)
createServer(app).listen(3000, () => {
console.log('Swagger UI is available at http://localhost:3000/docs')
})Only user-defined routes are merged from sub-routers. Internal routes like the OpenAPI schema
endpoint (/openapi.json) and Swagger UI (/docs) from sub-routers are not propagated. The
parent router has its own OpenAPI document that aggregates all merged routes automatically.