Kynetra FX

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.

app.ts
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.ts
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.ts
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.ts
app.get('/users/me', (ctx) => ctx.json({ self: true })) // wins for /users/me
app.get('/users/:id', (ctx) => ctx.json({ id: ctx.params.id })) // wins for /users/42
app.get('/users/*', (ctx) => ctx.text('catch-all')) // wins only if nothing else matches

Note

Registration order does not affect which route wins. The score is computed from the path pattern alone. This avoids subtle ordering bugs in large codebases.

HTTP method handlers#

app.ts
app.get('/items', listItems)
app.post('/items', createItem)
app.put('/items/:id', replaceItem)
app.patch('/items/:id', updateItem)
app.delete('/items/:id', deleteItem)
 
// Any method
app.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().

app.ts
import { requireAuth } from '@kynetra/fx-auth'
 
// Only authenticated users can access this route
app.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

Per-route middleware runs after app-level middleware. Use app-level middleware for cross-cutting concerns (logging, CORS, request IDs) and per-route middleware for route-specific concerns (auth, validation guards).

The contract route#

For routes with validated inputs and OpenAPI metadata, use app.route() instead of app.get/post:

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

response body
{
"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.