Nuxt Server Middleware Patterns: Logging, CORS, Request Context
AI generated
<v/>
{ }
Nuxt 3 · Nitro · Middleware · Backend
Nuxt Server Middleware Patterns
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.

17 min read server/middleware · H3Event · AsyncLocalStorage Nuxt 3 · Nuxt 4

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.

11. FAQ: Nuxt Server Middleware Patterns

1Server middleware vs. route middleware?
Server middleware runs at the HTTP level before every request. Route middleware runs inside the Vue Router before page navigation and knows redirects.
2Order of middleware files?
Alphabetical by file name. Numeric prefixes like 01., 02. make the order visible in the file system.
3Log request duration centrally?
Via the finish event of the Node response object, which only fires after the response is fully sent.
4Handle CORS preflight correctly?
Answer the OPTIONS request with status 204 and allowed methods, without reaching the actual route.
5Why AsyncLocalStorage?
Makes data available across an asynchronous call chain without threading it manually, useful for request ids.
6Access protection for API routes?
Yes, bearer token checks without redirect. Missing or invalid tokens throw a 401 error directly.
7What happens on an error in middleware?
The chain aborts immediately, subsequent middleware and the handler are never reached.
8Why no database queries in middleware?
Runs on every request, even static assets. Expensive operations create unnecessary latency.
9Does it run for static assets?
Yes, for every request reaching the Nitro server, regardless of the request's target.
10How do I test server middleware in isolation?
With integration tests via @nuxt/test-utils and $fetch against a running test server.