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.

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.