Documentation
Middleware
Kynetra FX middleware follows the onion model: each middleware receives control, can do work, calls await next() to pass inward, and then can do more work on the way back out — just like Koa. This gives you full control over both the request and the response in a single function.
The onion model#
When a request arrives, middleware is executed in registration order from outermost to innermost. After the terminal handler returns, control unwinds back through the middleware stack in reverse order.
import { createFX } from '@kynetra/fx' const app = createFX() app.use(async (ctx, next) => { console.log('before A') const res = await next() // pass control inward console.log('after A') return res}) app.use(async (ctx, next) => { console.log('before B') const res = await next() console.log('after B') return res}) app.get('/', (ctx) => { console.log('handler') return ctx.text('hi')}) // Log output for GET /:// before A// before B// handler// after B// after AWriting middleware#
A middleware is a function with the signature (ctx, next) => Response | Promise<Response>. Always return the result of await next() (or a replacement response) so the return value propagates correctly back up the stack.
import type { FXMiddleware } from '@kynetra/fx' export const timing: FXMiddleware = async (ctx, next) => { const start = Date.now() const res = await next() const ms = Date.now() - start // Add a Server-Timing header to the response return new Response(res.body, { status: res.status, headers: { ...Object.fromEntries(res.headers), 'Server-Timing': 'handler;dur=' + ms, }, })}import { timing } from './middleware/timing' app.use(timing)Short-circuiting#
Return a response from middleware without calling next() to short-circuit the request — the handler and any inner middleware will not run.
export const maintenanceMode: FXMiddleware = async (ctx, next) => { if (process.env.MAINTENANCE === 'true') { return ctx.json({ error: 'Service temporarily unavailable' }, { status: 503 }) } return next()}app.use()#
app.use(middleware) registers app-level middleware. All registered middleware runs in order for every matching request, before the route handler.
app.use(requestId())app.use(logger())app.use(cors({ origin: '*' }))Note
Built-in middleware#
All built-in middleware is exported from @kynetra/fx. Use them as factories (call them to produce the middleware function):
requestId()
Attaches a unique request ID to each request, available via the x-request-id response header and as a decoration on ctx. Uses the incoming x-request-id header if present, otherwise generates a UUID.
import { createFX, requestId } from '@kynetra/fx' const app = createFX()app.use(requestId())logger()
Logs each request with method, path, status code, and duration. Output goes to console.log and is edge-runtime safe.
import { createFX, logger } from '@kynetra/fx' const app = createFX()app.use(logger())secureHeaders()
Adds a sensible set of security headers to every response: X-Content-Type-Options,X-Frame-Options, Referrer-Policy,Permissions-Policy, and a conservative Content-Security-Policy.
import { createFX, secureHeaders } from '@kynetra/fx' const app = createFX()app.use(secureHeaders())cors()
Handles preflight OPTIONS requests and adds CORS headers to every response. Accepts an options object:
| Name | Type | Description |
|---|---|---|
| origin | string | string[] | ((origin: string) => boolean) | Allowed origins. Defaults to "*". |
| methods | string[] | Allowed HTTP methods. Defaults to common methods. |
| headers | string[] | Allowed request headers. |
| credentials | boolean | Set Access-Control-Allow-Credentials. Defaults to false. |
| maxAge | number | Max age in seconds for preflight cache. |
import { createFX, cors } from '@kynetra/fx' const app = createFX() app.use(cors({ origin: ['https://app.example.com', 'https://admin.example.com'], methods: ['GET', 'POST', 'PUT', 'DELETE'], headers: ['Content-Type', 'Authorization'], credentials: true, maxAge: 86400,}))Middleware ordering best practices#
requestId()— first, so all subsequent logs include the ID.logger()— second, so it captures timing across all middleware.secureHeaders()— early, so security headers are always set.cors()— before auth, so preflight requests (which have no auth) succeed.- Auth middleware — after CORS, before business logic.
- Custom middleware — after built-ins unless you need to run before them.
Group-scoped middleware#
Use g.use() inside app.group() to apply middleware only to routes in that group:
app.group('/admin', (g) => { g.use(requireAuth([jwtStrategy])) g.use(requireRole('admin')) g.get('/users', listAllUsers) g.delete('/users/:id', deleteUser)})See Groups for the full nesting and scoping reference.