Middleware
Middleware in Davaux is a function that runs before your route handler and can modify the request context, short-circuit the response, or pass control to the next handler in the chain. There are two places to register middleware, ordered from outermost to innermost.
App middleware — src/middleware.ts
Create src/middleware.ts (outside the routes directory) to register middleware that runs on every request — including requests that result in a 404. This is the right place for infrastructure concerns like sessions, security headers, CORS, and logging:
// src/middleware.ts
import { defineMiddleware } from 'davaux'
import { helmet } from '@davaux/helmet'
import { sessionMiddleware } from '@davaux/session'
import { csrfMiddleware } from '@davaux/csrf'
const helmetMw = helmet()
const sessionMw = sessionMiddleware({ secret: process.env.SESSION_SECRET! })
const csrfMw = csrfMiddleware()
export default defineMiddleware((ctx, next) =>
helmetMw(ctx, () => sessionMw(ctx, () => csrfMw(ctx, next)))
)
Because this file sits outside src/routes/, it is clearly separated from the routing tree and runs before any route matching occurs.
Note: Scoped
_middleware.tsdoes NOT run for 404 requests because there is no matched route. If you need to run code for every request including not-found ones, usesrc/middleware.ts.
Scoped middleware — _middleware.ts
Place a _middleware.ts file in any route directory. It runs for all routes in that directory and any subdirectories, but only when a route is matched:
// src/routes/dashboard/_middleware.ts
import { defineMiddleware, redirect } from 'davaux'
export default defineMiddleware(async (ctx, next) => {
const userId = ctx.state.session.get('userId')
if (!userId) {
redirect('/login')
}
const user = await db.users.findById(userId)
ctx.state.user = user
await next()
})
With this file in place, every page under /dashboard/ gets authentication checked automatically. The ctx.state.user value is available downstream.
Execution order
For a request to /dashboard/settings:
src/middleware.tssrc/routes/_middleware.ts(if present)src/routes/dashboard/_middleware.ts(if present)src/routes/dashboard/settings.page.tsx
Each layer calls next() to pass control to the next layer. If a layer does not call next(), the chain stops there.
Composition pattern
Multiple middleware can be chained in a single file by wrapping them. This is how src/middleware.ts chains packages that don't natively compose:
// src/middleware.ts
import { defineMiddleware } from 'davaux'
import { helmet } from '@davaux/helmet'
import { cors } from '@davaux/cors'
import { logger } from '@davaux/logger'
const helmetMw = helmet()
const corsMw = cors({ origin: process.env.ALLOWED_ORIGIN ?? '*' })
const logMw = logger()
export default defineMiddleware((ctx, next) =>
logMw(ctx, () => helmetMw(ctx, () => corsMw(ctx, next)))
)
For scoped middleware with multiple concerns:
// src/routes/api/_middleware.ts
import { defineMiddleware, redirect } from 'davaux'
import { rateLimit } from '@davaux/rate-limit'
const limit = rateLimit({ windowMs: 60_000, max: 100 })
export default defineMiddleware(async (ctx, next) => {
await limit(ctx, async () => {
const auth = ctx.req.headers['authorization']
if (!auth?.startsWith('Bearer ')) {
ctx.res.writeHead(401, { 'Content-Type': 'application/json' })
ctx.res.end(JSON.stringify({ error: 'Unauthorized' }))
return
}
ctx.state.apiUser = parseToken(auth.slice(7))
await next()
})
})
Passing data via ctx.state
ctx.state is the primary mechanism for middleware-to-handler communication. Augment the State interface for full TypeScript autocomplete:
// src/types.d.ts
import 'davaux'
declare module 'davaux' {
interface State {
user?: { id: string; name: string; role: 'admin' | 'member' }
requestId?: string
apiUser?: { sub: string; scopes: string[] }
}
}
With that declaration in place, ctx.state.user is correctly typed everywhere — in middleware, pages, and API handlers.
Short-circuiting
Write to ctx.res directly and skip calling next() to stop the chain:
export default defineMiddleware(async (ctx, next) => {
if (ctx.req.method === 'OPTIONS') {
ctx.res.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
})
ctx.res.end()
return // chain stops here
}
await next()
})
Or use redirect() to send a redirect response and stop the chain:
export default defineMiddleware(async (ctx, next) => {
if (!ctx.state.session.get('userId')) {
redirect('/login') // throws RedirectError — chain stops
}
await next()
})