In Nuxt 4, when does code in `server/middleware/` run compared with code in `server/plugins/`, and what happens if a server middleware returns a value?
answer
- per request versus per start
- sorted by file name
- decorate the event, don't answer
- hooks registered once
- a thrown hook does not block
basics
~20 sServer middleware runs on every request Nitro routes, before the route handler, and should only add context, set headers or throw; a returned value becomes the response and stops the request. Nitro plugins run once per server start to register hooks.
solid answer
~50 sFiles in Nuxt 4's `server/middleware/` are h3 handlers made with `defineEventHandler` that Nitro runs, sorted by file name, before every server route and page render. They are for inspecting or decorating the request: put the session's user on `event.context`, add a header, or `throw createError` to reject. If one returns a value, h3 sends that value as the response and nothing after it runs, so a stray `return { ok: true }` answers every page with that JSON. Files in `server/plugins/` export `defineNitroPlugin((nitroApp) => { ... })`, which runs once when a server instance starts, not per request. Plugins register runtime hooks such as `request`, `beforeResponse`, `afterResponse`, `error` and `close`, or mount storage drivers. They cannot replace middleware for access control: an error thrown inside a `request` hook is captured and the request carries on. Neither is Nuxt's route middleware in `app/middleware`, which runs in the Vue app on navigation.
code
ts · 11 lines// server/middleware/01.session.ts
export default defineEventHandler(async (event) => {
const token = getCookie(event, 'session')
event.context.user = token ? await findUser(token) : null
const path = getRequestURL(event).pathname
if (path.startsWith('/api/inventory') && !event.context.user) {
throw createError({ status: 401, statusText: 'Sign in first' })
}
// no return: the request continues to the route handler
})go deeper
Recall that server middleware runs on every request and should not return anything, while Nitro plugins in server/plugins run once when the server starts.
Explain the mechanics: file-name order, event.context for passing data, return values ending the request, and the hook names a plugin can register.
Show production judgment: scope middleware cheaply, put access checks in middleware not hooks, and avoid async plugin setup that the first requests race against.
Decide what cross-cutting concerns live in the Nuxt server at all, weighing per-request latency on page HTML against moving auth or auditing to a dedicated service.
## Three things called middleware or plugins Nuxt 4 has several extension points with overlapping names, so interviewers check that you keep them apart: | Where | Made with | Runs | Typical job | |---|---|---|---| | `server/middleware/*.ts` | `defineEventHandler` | on every request Nitro routes, before the handler | attach the user, add headers, reject early | | `server/plugins/*.ts` | `defineNitroPlugin` | once per server instance start | register runtime hooks, mount storage | | `app/middleware/*.ts` | `defineNuxtRouteMiddleware` | in the Vue app, on route navigation | redirect between pages | The third row belongs to the app's router, not to Nitro; it never sees an API call that a page makes. ## Server middleware: every request, in file order Nitro registers each file in `server/middleware/` as an h3 middleware layer ahead of the router. Consequences: - **It runs for every request that reaches Nitro's handlers**, page renders included, not just `/api` calls. A public file that Nitro's own static handler serves is answered before it. - **Order is the sorted file path.** `01.request-id.ts` runs before `02.session.ts`. The sort is by string, so `10.audit.ts` comes before `2.rate.ts`; pad the numbers. - **It should not answer the request.** A middleware that returns `undefined` lets the request continue. One that returns anything else ends the request: h3 sends the value as the response and no route handler runs. - **It shares state through `event.context`.** Whatever it puts there, such as `event.context.user`, is visible to the route handler for the same request. - **It can reject.** `throw createError({ status: 401 })` stops the request with that status. To limit a middleware to part of the site, test the path inside it (`getRequestURL(event).pathname.startsWith('/api/inventory')`), or attach logic to one route with h3's object syntax, `defineEventHandler({ onRequest: [requireUser], handler })`. ## Nitro plugins: once per instance, hooks for later A file in `server/plugins/` exports `defineNitroPlugin((nitroApp) => { ... })`. Nitro calls every plugin function once when the server instance boots, in file order. On a long-running Node server that is once per process; on serverless or edge runtimes it is once per cold start. Plugin code typically: 1. Registers runtime hooks with `nitroApp.hooks.hook(name, fn)`: `request` (every incoming request), `beforeResponse` and `afterResponse` (around sending), `error` (every captured error), `close` (shutdown), and in Nuxt `render:html` for server-rendered pages. 2. Mounts storage at runtime, for example `useStorage().mount('redis', driver)` with credentials read from runtime config. 3. Sets up clients that should exist once per instance. Two behaviours catch people out: - **Plugins are called, not awaited.** Nitro invokes each plugin function synchronously. If it is `async`, requests can arrive before its awaited work has finished. A synchronous throw, by contrast, aborts the server's startup. - **Hook errors do not block requests.** Nitro calls the `request` hook and routes any error it throws to `captureError` (which also fires the `error` hook), then continues handling the request. A hook is the right place for logging and metrics; it is the wrong place for an access check. ## Choosing between them in a backend-for-frontend For an inventory BFF inside Nuxt: - `server/middleware/01.session.ts` reads the session cookie, looks up the shopper and sets `event.context.user`; it throws 401 on `/api/inventory` paths when there is no session. - `server/plugins/audit.ts` hooks `afterResponse` to record which reservations were made, and `close` to flush that buffer on shutdown. - Route handlers read `event.context.user` and never parse the cookie themselves. ## Middleware or a hook: a quick test Ask what should happen when the code fails or decides against the request: | If the code must... | Put it in | |---|---| | stop the request (401, 403, 429) | server middleware, or `onRequest` on one route | | add data the handler reads | server middleware, via `event.context` | | observe every request without affecting it | a `request` or `afterResponse` hook in a plugin | | run once at startup or shutdown | a Nitro plugin, with a `close` hook for cleanup | Middleware is part of the request path, so its errors and its latency are the request's. A hook sits beside the request path: its errors are reported, not returned. ## Failure modes to name - A middleware that returns `true` or an object answers every page with it. - A slow lookup in middleware adds its latency to every request, including page HTML; cache the lookup or skip it for paths that do not need it. - An async plugin that opens a connection may not be ready for the first requests. - An auth check written as a `request` hook logs its error and lets the request through. - Two middleware files that both set `event.context.user` in the wrong file order, so the later one silently overwrites the first.
- How do you run a check before only one Nuxt 4 server route, without a global middleware?Use h3's object syntax for that route: `export default defineEventHandler({ onRequest: [requireUser], handler: async (event) => ... })`. Each `onRequest` function runs before the handler, and if one sends a response the handler is skipped. That keeps the check next to the route instead of adding a path test to middleware that every request pays for.
- Why might the first requests after a cold start fail when a Nitro plugin opens a database connection with await?Nitro calls plugin functions synchronously and does not wait for the promise an async plugin returns. Requests can reach handlers while the connection is still opening. Create the client lazily on first use in a `server/utils` helper, or have handlers await a shared ready promise, instead of relying on plugin order.
saying these in an interview costs you the question
- Server middleware only runs for requests under /api, never for page renders.
- Returning a value from server middleware is how you pass data to the route handler.
- A Nitro plugin runs on every request, like middleware.
- Nitro waits for an async plugin to finish before accepting requests.
- Throwing inside a request hook rejects the request with that error's status.
- Files in server/middleware and app/middleware are the same mechanism.