ServerRouter Composition

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.

users-router.ts
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 })
  })
app.ts
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.

users-router.ts
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 })
  })
app.ts
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:

users-router.ts
import { createRouter, Response } from 'fets'
 
export const usersRouter = createRouter({ base: '/users' }).route({
  path: '/:id',
  method: 'GET',
  handler: request => Response.json({ id: request.params.id })
})
app.ts
import { createRouter } from 'fets'
import { usersRouter } from './users-router'
 
const app = createRouter().use(usersRouter)
// GET /users/:id → usersRouter's '/:id' handler

Composing 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' handler

Full Example

Here is a complete example showing how to organise a larger API using router composition:

users-router.ts
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'] })
})
posts-router.ts
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'] })
})
app.ts
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.