server/middleware/
Middleware files run on every HTTP request before your route handlers. Use them for cross-cutting concerns: logging, cors, rate limiting, request timing, auth checks that apply globally.
Files in server/middleware/ are loaded in filename order and run before every request.
Defining middleware
// server/middleware/logging.ts
export default defineEventHandler(event => {
const start = Date.now()
console.log(`→ ${event.method} ${event.path}`)
// Attach timing header on response
event.node.res.on("finish", () => {
console.log(`← ${event.method} ${event.path} ${Date.now() - start}ms`)
})
})
Middleware receives the event and can:
- Return nothing to pass through to the next handler
- Throw a
createErrorto short-circuit and return an error response
Order of execution
"request → middleware/a.ts → middleware/b.ts → route handler"
If you need specific ordering, prefix filenames with numbers:
"server/middleware/01-logging.ts"
"server/middleware/02-cors.ts"
"server/middleware/99-timing.ts"
Practical examples
CORS
// server/middleware/cors.ts
import { defineEventHandler, setHeader } from "h3"
export default defineEventHandler(event => {
setHeader(event, "Access-Control-Allow-Origin", "*")
setHeader(event, "Access-Control-Allow-Methods", "GET, POST, OPTIONS")
setHeader(event, "Access-Control-Allow-Headers", "Content-Type, Authorization")
})
Rate limiting
// server/middleware/rate-limit.ts
import { defineEventHandler, createError } from "h3"
const requests = new Map<string, number[]>() // ip → [timestamps]
export default defineEventHandler(event => {
const ip = event.node.req.socket.remoteAddress ?? "unknown"
const now = Date.now()
const window = 60000 // 1 minute
const limit = 60 // requests per minute
const timestamps = requests.get(ip) ?? []
const recent = timestamps.filter(t => now - t < window)
if (recent.length >= limit) {
throw createError({ statusCode: 429, message: "rate limit exceeded" })
}
recent.push(now)
requests.set(ip, recent)
})
Request validation
// server/middleware/api-key.ts
import { defineEventHandler, getRequestHeader, createError } from "h3"
export default defineEventHandler(event => {
const key = getRequestHeader(event, "x-api-key")
if (!key || !key.startsWith("axon_")) {
throw createError({ statusCode: 401, message: "invalid api key" })
}
})
Note: Deployed routes already require auth by default. Use middleware for additional checks, not for the default auth.
What middleware cannot do
- Middleware cannot modify the response body after the handler runs.
- For async initialization (connecting to services), use plugins instead — plugins run once at boot, middleware runs on every request.