Middleware
Intercept requests and responses with composable middleware functions.
Introduction
Middleware are async functions that sit between the incoming HTTP request and your route handler. They can inspect or modify the request, run side effects (logging, authentication, rate limiting), modify the response, or abort the chain entirely by returning a Response without calling next().
Every middleware has exactly this signature:
import type { HttpContext } from '@tekir/core'
// Every middleware has this exact signature:
type MiddlewareFunction = (
ctx: HttpContext,
next: () => Promise<void>
) => void | Response | Promise<void | Response>Before and After Pattern
Middleware form an onion: each layer wraps the next. Code before await next() runs on the way in; code after await next() runs on the way out, after the handler has already produced a result.
Request arrives
│
▼
[global middleware 1] ← useGlobal (CORS, session)
│ await next()
▼
[global middleware 2]
│ await next()
▼
[router middleware 1] ← useRouter (logging, request-id)
│ await next()
▼
[route middleware 1] ← .use() on the route / @Middleware on the method
│ await next()
▼
[handler] ← your controller method or inline function
│ returns result
▼
[route middleware 1] ← code after await next()
│
▼
[router middleware 1]
│
▼
[global middleware 2]
│
▼
[global middleware 1]
│
Response senttekir has three stacking levels, executed in this order:
- Server-level (
router.useGlobal()): runs on every request, even if no route matches. - Router-level (
router.useRouter()): runs only on requests that match a registered route. - Route-level (
.use()or@Middleware): runs only for a specific route or controller method.
Creating a Middleware
Generate a middleware with the CLI:
tekir make:middleware RequestLoggerA middleware is a plain async function. Export it as the default and import it where needed.
Here is a request logger that measures how long each request takes:
import { logger } from '#services'
import type { HttpContext } from '@tekir/core'
// "before" code runs before await next()
// "after" code runs after await next()
export default async function requestLogger(ctx: HttpContext, next: () => Promise<void>) {
const start = performance.now()
await next() // run the next middleware + handler
const ms = (performance.now() - start).toFixed(2)
logger.info(`${ctx.request.method} ${ctx.route.pattern} ${ms}ms`)
}And here is an authentication guard that reads a Bearer token and attaches the decoded user to ctx.store so handlers downstream can access it:
import type { HttpContext } from '@tekir/core'
export default async function authGuard(ctx: HttpContext, next: () => Promise<void>) {
const token = ctx.headers['authorization']?.replace('Bearer ', '')
if (!token) return ctx.response.unauthorized({ message: 'Missing token' })
// Decode / verify token...
const user = await verifyJwt(token)
if (!user) return ctx.response.unauthorized({ message: 'Invalid token' })
// Attach user to context so downstream handlers can read it
ctx.store.user = user
await next()
}The kernel.ts File
start/kernel.ts is the central place to wire up providers, global middleware, and named middleware. It runs once at application boot, before the first request arrives.
import type { TekirApp } from '@tekir/core'
import { cors } from '@tekir/cors'
import { session } from '@tekir/session'
import { AuthProvider } from '@tekir/auth'
import { DatabaseProvider } from '@tekir/db'
import { CacheProvider } from '@tekir/cache'
import { ViewProvider } from '@tekir/view'
import requestLogger from '~/middleware/request_logger'
import addRequestId from '~/middleware/add_request_id'
export default function({ router, app, config }: TekirApp) {
/*
* Service providers, boot packages like DB, cache, auth
*/
app.registerAll([
DatabaseProvider,
CacheProvider,
AuthProvider,
ViewProvider
])
/*
* Server-level middleware
* Runs on EVERY request, even ones that don't match any route.
* Use for CORS, sessions, and anything truly global.
*/
router.useGlobal([
cors(config('cors')),
session(config('session'))
])
/*
* Router-level middleware
* Runs only when a route is matched.
* Use for logging, request IDs, and app-wide guards.
*/
router.useRouter([
addRequestId,
requestLogger
])
}Server-level Middleware
Register with router.useGlobal(middleware). Accepts a single function or an array. Use this for cross-cutting concerns like CORS headers and session cookies that must be present even on 404 responses.
Router-level Middleware
Register with router.useRouter(middleware). These run only after a route has been matched, so they have access to ctx.route.pattern and route parameters. Logging, request IDs, and application-wide auth guards belong here.
After Middleware (ctx.$result)
After await next()returns, the handler's return value is stored in ctx.$result. Middleware can read and replace this value before tekir serialises it into a Response. This is the correct way to add headers to every outgoing response, wrap responses in an envelope, or transform the result.
import type { HttpContext } from '@tekir/core'
export default async function addRequestId(ctx: HttpContext, next: () => Promise<void>) {
// BEFORE: generate a unique ID and store it
const requestId = crypto.randomUUID()
ctx.store.requestId = requestId
await next() // the handler runs, result is stored in ctx.$result
// AFTER: attach the ID to the outgoing response
const result = ctx.$result
if (result instanceof Response) {
result.headers.set('X-Request-Id', requestId)
} else if (result && typeof result === 'object') {
// If the handler returned a plain object, wrap it in a Response
ctx.$result = Response.json(result, {
headers: { 'X-Request-Id': requestId, 'Content-Type': 'application/json' }
})
}
}Attaching Middleware to Routes
Route-level middleware runs after global and router middleware. You can attach it three ways:
import authGuard from '~/middleware/auth_guard'
// On a standalone route
router.get('/secret', handler).use(authGuard)
// In a group: all routes in the group get the middleware
router.group(() => {
router.get('/dashboard', showDashboard)
router.post('/settings', updateSettings)
}).prefix('/admin').use(authGuard)
// Multiple middleware: executed left to right
router.post('/posts', createPost).use([authGuard, rateLimiter])Short-circuiting the Chain
Return a Response (or call a response helper and return its result) from middleware without calling next() to abort the chain. No subsequent middleware or the handler will run.
import type { HttpContext } from '@tekir/core'
import env from '#env'
export default async function maintenanceMode(ctx: HttpContext, next: () => Promise<void>) {
if (env.MAINTENANCE) {
// Return early WITHOUT calling next(): the chain stops here
return ctx.response.serviceUnavailable({ message: 'We are down for maintenance.' })
}
// Normal operation: continue chain
await next()
}