Kynetra FX

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.

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

Writing 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.

middleware/timing.ts
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,
},
})
}
app.ts
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.

middleware/maintenance.ts
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.ts
app.use(requestId())
app.use(logger())
app.use(cors({ origin: '*' }))

Note

Register app-level middleware before routes. While Kynetra FX resolves routes by match score (not registration order), middleware IS executed in registration order.

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.

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

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

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

NameTypeDescription
originstring | string[] | ((origin: string) => boolean)Allowed origins. Defaults to "*".
methodsstring[]Allowed HTTP methods. Defaults to common methods.
headersstring[]Allowed request headers.
credentialsbooleanSet Access-Control-Allow-Credentials. Defaults to false.
maxAgenumberMax age in seconds for preflight cache.
app.ts
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.ts
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.