Documentation
Groups & Nesting
app.group(prefix, callback) creates a sub-router scoped to a URL prefix. Routes and middleware registered inside the callback are only active for paths that start with that prefix. Groups can be nested arbitrarily.
Basic groups#
Pass a path prefix and a callback that receives the group router g. All paths registered on g are automatically prefixed.
import { createFX } from '@kynetra/fx' const app = createFX() app.group('/users', (g) => { g.get('/', listUsers) // matches GET /users g.post('/', createUser) // matches POST /users g.get('/:id', getUser) // matches GET /users/:id g.put('/:id', updateUser) // matches PUT /users/:id g.delete('/:id', deleteUser)// matches DELETE /users/:id})Scoped middleware#
Middleware registered with g.use() only applies to routes within that group. It runs after app-level middleware and before per-route middleware in the onion order.
import { createFX, requestId, logger } from '@kynetra/fx'import { jwtAuth, requireAuth } from '@kynetra/fx-auth' const app = createFX() // App-level — runs for every requestapp.use(requestId())app.use(logger()) // Require auth for all /api/* routesapp.group('/api', (g) => { g.use(requireAuth([jwtAuth({ secret: 'my-secret' })])) g.get('/me', (ctx) => ctx.json({ user: 'me' })) g.get('/settings', (ctx) => ctx.json({ theme: 'dark' }))}) // Public routes — no auth requiredapp.get('/health', (ctx) => ctx.text('ok'))Nesting groups#
Groups can be nested to any depth. The prefixes are concatenated left-to-right.
app.group('/api', (api) => { api.group('/v1', (v1) => { v1.group('/users', (users) => { users.get('/', listUsers) // GET /api/v1/users users.post('/', createUser) // POST /api/v1/users users.get('/:id', getUser) // GET /api/v1/users/:id users.group('/:userId/posts', (posts) => { posts.get('/', listUserPosts) // GET /api/v1/users/:userId/posts posts.post('/', createPost) // POST /api/v1/users/:userId/posts }) }) })})Tip
Route modules with groups#
The idiomatic way to use groups in larger projects is to define each group in its own module and call it from app.ts:
import type { FXApp } from '@kynetra/fx' export function userRoutes(app: FXApp) { app.group('/users', (g) => { g.get('/', listUsers) g.post('/', createUser) g.get('/:id', getUser) })} function listUsers(ctx: any) { return ctx.json([]) }function createUser(ctx: any) { return ctx.json({}, { status: 201 }) }function getUser(ctx: any) { return ctx.json({ id: ctx.params.id }) }import { createFX } from '@kynetra/fx'import { userRoutes } from './routes/users'import { authRoutes } from './routes/auth' const app = createFX() userRoutes(app)authRoutes(app) export { app }Combining group middleware with per-route middleware#
Group middleware, per-route middleware, and app-level middleware all compose correctly. The execution order for a request to GET /api/users/42 with the setup below is:
- App-level:
requestId() - App-level:
logger() - Group-level (
/api):requireAuth() - Per-route:
checkPermissions() - Handler:
getUser()
app.use(requestId())app.use(logger()) app.group('/api', (g) => { g.use(requireAuth([jwtStrategy])) // checkPermissions runs before getUser, after group auth g.get('/users/:id', checkPermissions, getUser)})Groups vs app.route()#
app.group() and app.route() (the contract layer) are complementary. You can use app.route() inside a group callback:
import { createFX, fx } from '@kynetra/fx' const app = createFX() app.group('/api/v2', (g) => { g.route({ method: 'POST', path: '/users', input: fx.object({ name: fx.string(), email: fx.string() }), handler(ctx) { const { name, email } = ctx.input return ctx.json({ name, email }, { status: 201 }) }, })})See Contracts for the full app.route() reference.