Documentation
Routing
Kynetra FX routes are registered with method-specific helpers: app.get, app.post, app.put, app.patch, app.delete, and app.all for any method. Routes are matched by a scored algorithm — static segments beat dynamic params, which beat wildcards.
Static routes#
A static route matches exactly one URL path. It is the most specific kind and always wins when it matches.
import { createFX } from '@kynetra/fx' const app = createFX() app.get('/', (ctx) => ctx.json({ page: 'home' }))app.get('/health', (ctx) => ctx.text('ok'))app.get('/about', (ctx) => ctx.html('<h1>About</h1>'))Dynamic (param) routes#
Prefix a segment with : to make it a named parameter. The captured value is available on ctx.params.
app.get('/users/:id', (ctx) => { const { id } = ctx.params return ctx.json({ id })}) app.get('/orgs/:orgId/repos/:repoId', (ctx) => { const { orgId, repoId } = ctx.params return ctx.json({ orgId, repoId })})See Params for the full reference including query strings, wildcard capture, and URL decoding.
Wildcard routes#
A trailing * matches any suffix. The matched portion is available as ctx.params['*'].
app.get('/files/*', (ctx) => { const path = ctx.params['*'] // e.g. 'assets/logo.png' return ctx.text('serving: ' + path)})Match-score precedence#
When multiple routes could match a URL, Kynetra FX picks the most specific one using a score calculated per segment:
- Static segment (e.g.
/users) — highest score. - Dynamic param (e.g.
/:id) — medium score. - Wildcard (
*) — lowest score.
app.get('/users/me', (ctx) => ctx.json({ self: true })) // wins for /users/meapp.get('/users/:id', (ctx) => ctx.json({ id: ctx.params.id })) // wins for /users/42app.get('/users/*', (ctx) => ctx.text('catch-all')) // wins only if nothing else matchesNote
HTTP method handlers#
app.get('/items', listItems)app.post('/items', createItem)app.put('/items/:id', replaceItem)app.patch('/items/:id', updateItem)app.delete('/items/:id', deleteItem) // Any methodapp.all('/webhook', handleWebhook)app.all matches every HTTP method on the path. Use it for webhooks that need to handle both GET and POST, or as a generic catch-all.
Per-route middleware#
Pass additional handlers before the terminal handler to run middleware only on that route. The last argument is always the terminal handler; earlier arguments are middleware that must call await next().
import { requireAuth } from '@kynetra/fx-auth' // Only authenticated users can access this routeapp.get('/profile', requireAuth([jwtStrategy]), (ctx) => { return ctx.json({ user: 'Alice' })}) // Multiple per-route middleware — each calls next()app.post( '/admin/users', requireAuth([jwtStrategy]), requireRole('admin'), async (ctx) => { return ctx.json({ created: true }, { status: 201 }) })Tip
The contract route#
For routes with validated inputs and OpenAPI metadata, use app.route() instead of app.get/post:
import { createFX, fx } from '@kynetra/fx' const app = createFX() app.route({ method: 'GET', path: '/users/:id', query: fx.object({ include: fx.optional(fx.string()) }), operationId: 'getUser', summary: 'Get a user by ID', handler(ctx) { const { id } = ctx.params const { include } = ctx.validatedQuery return ctx.json({ id, include }) },})See Contracts for the full app.route() reference.
404 handling#
If no route matches the request, Kynetra FX automatically returns a JSON 404 response:
{ "error": { "code": "FX_NOT_FOUND", "message": "No route matched GET /unknown" }}To customise the 404 response, register an onError hook and check for the FX_NOT_FOUND error code. See Error Handling.