Logging, CORS and Request Context without repetition
Implementing logging, CORS headers or request IDs separately in every single Nuxt Server Route produces code duplication in ten places at once. Nuxt Server Middleware in server/middleware runs before every handler and solves exactly these cross cutting concerns centrally, without any route knowing about it.
Table of Contents
- 1. Server middleware vs. route middleware
- 2. Execution order and file naming
- 3. Request logging as middleware
- 4. Solving CORS centrally
- 5. Request context with AsyncLocalStorage
- 6. Auth header checks without a redirect
- 7. Error handling in the middleware chain
- 8. Performance and when to avoid middleware
- 9. Middleware patterns compared
- 10. Summary
- 11. FAQ
1. Server middleware vs. route middleware
Nuxt has two entirely different concepts that both happen to be called "middleware", and this overlap regularly causes confusion. Route middleware in middleware/ runs inside the Vue Router on both client and server before navigating to a page, and decides whether a route gets loaded, redirected or blocked. Nuxt Server Middleware in server/middleware/ is something fundamentally different: it runs at the HTTP level, before every single request that reaches the Nitro server, whether that request is asking for a page, an API route, or a static file.
This distinction matters because Nuxt Server Middleware knows nothing about Vue components, route names or page metadata. It operates purely on the H3Event, the raw request and response object. That makes it ideal for tasks that must apply to the whole application, regardless of whether the end result is a rendered page or a JSON response: logging, CORS headers, request IDs, rate limiting counters, or rejecting obviously malformed requests early.
A team unaware of this distinction often tries to implement access control in Nuxt Server Middleware where route middleware or an auth module would actually be the better fit. For pure API endpoints, however, server middleware is exactly the right place for uniform cross cutting concerns that need to apply before every route's actual business logic.
2. Execution order and file naming
Every file in server/middleware/ runs automatically on every incoming request, with no registration needed anywhere. Execution order follows the alphabetical sort of the file names, which is why a common Nuxt Server Middleware pattern uses numeric prefixes: 01.logger.ts, 02.cors.ts, 03.request-id.ts. These prefixes make the execution order visible right in the file system, without having to search through a separate configuration file.
Unlike a regular event handler, a Nuxt Server Middleware normally does not return a value. It reads or modifies the event, sets headers, or throws a createError on failure. As long as no value is returned and no error thrown, the chain continues to the next middleware and eventually to the actual route handler. This implicit chaining model is deliberately minimal compared to Express, where next() has to be called explicitly.
// server/middleware/01.logger.ts
// Runs first for every incoming request
export default defineEventHandler((event) => {
const start = Date.now()
event.node.res.on('finish', () => {
const duration = Date.now() - start
console.log(
`[${event.node.req.method}] ${event.node.req.url} — ${event.node.res.statusCode} (${duration}ms)`
)
})
})
// server/middleware/02.cors.ts
// Runs second, before route handlers see the request
export default defineEventHandler((event) => {
setHeader(event, 'Access-Control-Allow-Origin', 'https://shop.mironsoft.de')
setHeader(event, 'Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
if (event.node.req.method === 'OPTIONS') {
setResponseStatus(event, 204)
return ''
}
})
3. Request logging as middleware
Structured request logging is the classic entry point into Nuxt Server Middleware, because the benefit is immediately visible: every request, every method, every status code and every response time lands in a single log line, without having to change a single route for it. The trick lies in the finish event of the underlying Node response object, which only fires once the response has been fully sent, so status code and duration are captured correctly.
In production, structured JSON logging is worth the extra effort over plain text lines, so a log aggregator like Loki or Datadog can index the fields directly. A logging Nuxt Server Middleware should additionally strip sensitive headers such as authorization or cookie from the logged object before they end up anywhere, otherwise credentials accidentally travel into log files that are read by more people than the actual request itself.
// server/middleware/01.logger.ts
// Structured logging with sensitive headers stripped
export default defineEventHandler((event) => {
const start = Date.now()
const { method, url } = event.node.req
event.node.res.on('finish', () => {
const headers = { ...getHeaders(event) }
delete headers.authorization
delete headers.cookie
console.log(JSON.stringify({
method,
url,
status: event.node.res.statusCode,
durationMs: Date.now() - start,
requestId: event.context.requestId,
}))
})
})
4. Solving CORS centrally
CORS errors are one of the most common sources of frustration when a frontend runs on a different domain than the Nuxt Server Route API. Instead of setting CORS headers manually in every single route, this logic belongs in a single Nuxt Server Middleware that consistently runs before all API routes. Correctly handling preflight requests matters here: browsers automatically send an OPTIONS request before certain cross origin requests, which must be answered with status 204 and the allowed methods, without ever reaching the actual route.
For multi tenant applications with several allowed origins, a single static Access-Control-Allow-Origin header is not enough. The Nuxt Server Middleware must then check the request's Origin header against a whitelist and dynamically reflect back the matching value, instead of setting a blanket *, which the browser rejects anyway for requests with credentials.
// server/middleware/02.cors.ts
const allowedOrigins = new Set([
'https://shop.mironsoft.de',
'https://admin.mironsoft.de',
])
export default defineEventHandler((event) => {
const origin = getHeader(event, 'origin')
if (origin && allowedOrigins.has(origin)) {
setHeader(event, 'Access-Control-Allow-Origin', origin)
setHeader(event, 'Access-Control-Allow-Credentials', 'true')
setHeader(event, 'Vary', 'Origin')
}
setHeader(event, 'Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS')
setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization')
// Preflight request: answer immediately, never reach the actual route
if (event.node.req.method === 'OPTIONS') {
setResponseStatus(event, 204)
return ''
}
})
5. Request context with AsyncLocalStorage
An advanced use of Nuxt Server Middleware is creating a request context that stays available across the whole processing chain, without every function having to explicitly pass the request as a parameter. Node ships AsyncLocalStorage for exactly this: a middleware generates a unique request id at the start of each request, stores it in the storage, and any deeper function, even services with no access to the H3Event, can read it via asyncLocalStorage.getStore().
The practical benefit shows up when debugging distributed logs: if an error surfaces in a deeply nested service function, the request id makes it possible to trace exactly which incoming request the error belongs to, even when several layers sit between the Nuxt Server Middleware and the failing function. Without this mechanism, the request id would have to be threaded manually through every function signature, needlessly bloating the code.
// server/utils/requestContext.ts
import { AsyncLocalStorage } from 'node:async_hooks'
import { randomUUID } from 'node:crypto'
interface RequestContext {
requestId: string
}
export const requestContext = new AsyncLocalStorage<RequestContext>()
// server/middleware/00.request-context.ts
// Must run before logger and any route handler
export default defineEventHandler((event) => {
const requestId = getHeader(event, 'x-request-id') ?? randomUUID()
event.context.requestId = requestId
setHeader(event, 'X-Request-Id', requestId)
})
// Usage deep inside a service, without passing the event around
export function logWithContext(message: string) {
const store = requestContext.getStore()
console.log(`[${store?.requestId ?? 'no-request'}] ${message}`)
}
6. Auth header checks without a redirect
For pure API endpoints that have no concept of redirecting to a login page, a lightweight Nuxt Server Middleware that checks the auth header is often sufficient, without needing a full auth module. The middleware checks whether a bearer token is present and syntactically valid, and stores the decoded user in event.context so subsequent route handlers can use it without parsing again. If the token is missing on a protected route, the middleware throws a 401 error directly instead of triggering a redirect, which would not make sense for a JSON API anyway.
It matters that this middleware works selectively: not every route should require authentication, public endpoints like health checks or public product listings need to remain excluded. A simple pattern checks the request path against a list of protected prefixes before the actual token check even runs, so public Nuxt Server Routes stay reachable unchanged.
// server/middleware/03.auth-check.ts
const protectedPrefixes = ['/api/admin', '/api/orders']
export default defineEventHandler(async (event) => {
const path = event.node.req.url ?? ''
const isProtected = protectedPrefixes.some((prefix) => path.startsWith(prefix))
if (!isProtected) return
const authHeader = getHeader(event, 'authorization')
if (!authHeader?.startsWith('Bearer ')) {
throw createError({ statusCode: 401, statusMessage: 'Missing bearer token' })
}
const token = authHeader.slice('Bearer '.length)
const user = await verifyToken(token) // throws on invalid/expired token
event.context.user = user
})
7. Error handling in the middleware chain
An error thrown inside a Nuxt Server Middleware interrupts the chain immediately, and neither subsequent middleware nor the actual route handler get reached. That is usually desired, for example on a failed auth check, but it can produce unexpected results if a logging middleware sits after the failed auth middleware and therefore never sees the request at all.
So the order in server/middleware/ decides not only execution but also which middleware even gets to see a failed request. A proven pattern: place logging and request context as early as possible, so they capture every request, including ones that later fail an auth check. A global error handler in server/plugins/ additionally catches all unhandled errors, whether they originate from Nuxt Server Middleware or from a route handler.
8. Performance and when to avoid middleware
Since every file in server/middleware/ runs on every single request, regardless of whether the route even needs it, the overhead adds up with every additional Nuxt Server Middleware. For static assets that Nitro serves directly from the file system, the middleware chain still runs, which creates unnecessary latency for expensive checks. Costly operations like database queries therefore do not belong in globally running middleware, but in the specific route handler that actually needs them.
A proven rule of thumb: Nuxt Server Middleware should only be used for tasks relevant to virtually every request, such as logging, CORS or request ids. Anything that only applies to a subset of routes, such as specific validation or resource intensive checks, belongs as an explicit call inside the relevant handler, where the condition stays visible instead of implicitly disappearing into a globally running file.
9. Middleware patterns compared
Not every cross cutting concern belongs in the same layer. The table below maps typical tasks to the fitting mechanism and shows when Nuxt Server Middleware is actually the right choice and when a different concept fits better.
| Task | Mechanism | Runs for | Reasoning |
|---|---|---|---|
| Request logging | Server middleware | Every request | Centralized, no route awareness needed |
| CORS headers | Server middleware | Every API request | Uniform origin checking |
| Body validation | Route handler | Only affected route | Schema differs per route |
| Page access protection | Route middleware (client/server) | Page navigation only | Knows Vue Router and redirects |
| API token check | Server middleware | Protected API prefixes | No redirect concept in JSON APIs |
The table makes clear that Nuxt Server Middleware plays to its strengths wherever a task is relevant for virtually every request at the HTTP level, independent of Vue components or page navigation. For anything page specific or that triggers browser redirects, route middleware remains the right choice.
Mironsoft
Vue.js and Nuxt development for productive frontends and backends
Logging and CORS copied in ten places?
We build Nuxt Server Middleware that solves logging, CORS and request context centrally, so your API routes stay lean and focused.
Middleware audit
Consolidating existing cross cutting logic into server/middleware
Observability
Request context and structured logging for production debugging
Security
CORS and token checks without duplicated code in every route
10. Summary
Nuxt Server Middleware in server/middleware/ is the right place for tasks that apply at the HTTP level to virtually every request: logging, CORS headers, request context and simple header based access checks. It differs fundamentally from route middleware, which runs inside the Vue Router and controls page navigation. Execution order follows alphabetical file sorting, which is why numeric prefixes like 01., 02. make the order visible in the file system.
AsyncLocalStorage enables request context that stays available across the whole processing chain, without every function explicitly passing the request along. Because every Nuxt Server Middleware runs on every request, expensive operations do not belong here, but in the specific route handler. Respecting these boundaries results in an API where cross cutting logic stays centralized and business logic stays local to the respective routes.
Nuxt Server Middleware Patterns — The Essentials at a Glance
Order
Alphabetical by file name, numeric prefixes like 01., 02. make it explicit.
Use cases
Logging, CORS, request ids, simple token checks, anything at the HTTP level for every request.
Context
AsyncLocalStorage shares request id and user data without threading parameters.
Performance
Expensive operations belong in the route handler, not in globally running middleware.