Kynetra FX

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.

app.ts
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.

app.ts
import { createFX, requestId, logger } from '@kynetra/fx'
import { jwtAuth, requireAuth } from '@kynetra/fx-auth'
 
const app = createFX()
 
// App-level — runs for every request
app.use(requestId())
app.use(logger())
 
// Require auth for all /api/* routes
app.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 required
app.get('/health', (ctx) => ctx.text('ok'))

Nesting groups#

Groups can be nested to any depth. The prefixes are concatenated left-to-right.

app.ts
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

Deep nesting can make code harder to read. Prefer extracting nested groups into separate route modules. See Project Structure for the recommended pattern.

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:

src/routes/users.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 }) }
src/app.ts
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.ts
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:

app.ts
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.

Groups & Nesting · Kynetra FX