skip to content

Server Routes & Nitro

Nitro turns server/api and server/routes into H3 handlers that deploy to Node, serverless or edge. Interviewers ask whether you can build the backend-for-frontend inside Nuxt.

on this pageshow

explore

questions

6

In Nuxt 4, what is the difference between a handler in `server/api/` and one in `server/routes/`, and how do file names become URLs?

level: juniorimportance: must knowfreq 44%

answer

  1. one folder adds a prefix
  2. server/ stays beside app/
  3. brackets become parameters
  4. a verb before the extension
  5. JSON or HTML when it throws

basics

~20 s

Both folders hold Nitro handlers exported with defineEventHandler. Files in server/api are served under /api, files in server/routes at the bare path; brackets in a file name become parameters and a .get or .post suffix limits the method.

solid answer

~50 s

In Nuxt 4 the `server/` directory sits at the project root beside `app/`, and Nitro, Nuxt's server engine, turns every file in it into an h3 handler exported with `defineEventHandler`. The routing difference between the two folders is the prefix: `server/api/inventory/[sku].get.ts` answers `GET /api/inventory/:sku`, while `server/routes/sitemap.xml.ts` answers `/sitemap.xml`. Brackets become parameters, `[...path]` catches the rest of the path, `index` maps to the folder itself, and a `.get` or `.post` suffix registers the handler for that method only. The prefix also changes error bodies: Nuxt answers a failing `/api/` request with JSON unless the client asks for HTML, and renders its HTML error page elsewhere unless the request signals JSON. So use `server/api` for the JSON endpoints your pages call and `server/routes` for URLs whose shape is fixed from outside, such as a sitemap, a feed or a webhook path.

code

ts · 5 lines
ts
// server/api/inventory/[sku].get.ts  ->  GET /api/inventory/:sku
export default defineEventHandler((event) => {
  const sku = getRouterParam(event, 'sku')
  return { sku, available: 12 }
})

go deeper

for a junior

Recall that server/ stays at the project root in Nuxt 4, that server/api adds an /api prefix and server/routes does not, and that brackets and .get/.post suffixes shape the route.

for a middle

Explain the mapping from file path to route and method, catch-alls and index files, and why error bodies come back as JSON under /api but as an HTML page on other paths.

for a senior

Show where each endpoint belongs: fixed external URLs in server/routes, app-facing JSON in server/api, dev-only debug handlers behind .dev, and shared types in shared/ instead of cross-imports.

for a principal

Frame server/api as a public HTTP surface that ships with every deploy, and decide which URLs your organisation promises to outsiders versus which only your pages consume.

## Where server code lives in Nuxt 4 Nuxt 4 moved the Vue application into `app/`: pages, components, composables, app middleware and app plugins live there. The **server directory did not move**. `server/` sits at the project root beside `app/`, `public/` and `shared/`, and Nuxt resolves it as `<rootDir>/server`. Nitro, the server engine inside Nuxt, scans that folder in the dev server and at build time and turns every file into an **h3 handler**: a function that receives an `event` (the request and the response wrapped together) and produces the response. The same build can then run as a Node server, a serverless function or an edge worker, depending on the Nitro preset. Every route file default-exports a handler made with `defineEventHandler` (alias `eventHandler`). Returning an object sends it as JSON; returning a string sends the string. `defineEventHandler`, the h3 request helpers and whatever you export from `server/utils/` are auto-imported inside `server/`. ## How a file name becomes a URL Nitro derives the route from the file's path inside its folder: | File | URL | Methods | |---|---|---| | `server/api/inventory/index.get.ts` | `/api/inventory` | GET | | `server/api/inventory/[sku].get.ts` | `/api/inventory/:sku` | GET | | `server/api/inventory/[sku]/reserve.post.ts` | `/api/inventory/:sku/reserve` | POST | | `server/api/inventory/[...path].ts` | `/api/inventory/**` | every method | | `server/routes/sitemap.xml.ts` | `/sitemap.xml` | every method | | `server/routes/health.ts` | `/health` | every method | The rules behind the table: - **Brackets** make a parameter: `[sku]` is read in the handler with `getRouterParam(event, 'sku')`. - **`[...]` or `[...name]`** catches the rest of the path; the remainder lands in `event.context.params._` or under the name you gave. - **`index`** maps to the folder's own path. - **A method suffix** such as `.get`, `.post`, `.put`, `.patch` or `.delete` registers the handler for that method only; without one, the handler answers every method. - **An environment suffix** after the method (`.dev`, `.prod` or `.prerender`) registers the handler only in that environment, for example `debug.get.dev.ts`. - **Only the final extension is stripped**, which is why `sitemap.xml.ts` serves `/sitemap.xml`. ## `server/api` versus `server/routes` Both folders hold the same kind of handler. They differ in two ways: 1. **The URL prefix.** Files in `server/api` are mounted under `/api`; Nitro's `apiBaseURL` option, set under the `nitro` key of `nuxt.config.ts`, changes that prefix. Files in `server/routes` are mounted at the bare path. 2. **How errors come back.** When a handler throws, Nuxt decides whether the caller wants JSON. A path starting with `/api/` counts as a JSON request unless the `Accept` header asks for `text/html`, so the caller gets a JSON body with `statusCode`, `statusMessage` and `message`. On other paths, including everything in `server/routes`, Nuxt renders the app's HTML error page unless the request signals JSON, for example with `Accept: application/json` or a path ending in `.json`. That gives a simple placement rule: - **`server/api`** for the JSON endpoints your own pages call, such as a backend-for-frontend that proxies an inventory service. - **`server/routes`** for URLs whose shape is decided elsewhere: `/sitemap.xml`, `/robots.txt`, a feed, a health check, or a webhook path you gave to another system. ## Keeping app code and server code apart `server/` is compiled into the server bundle with its own auto-imports and its own generated TypeScript config. Nuxt's docs warn against importing Vue app code (components, composables) into server routes, and against importing server-only code into the app. Types and pure helpers that both sides need belong in the root `shared/` directory, whose `shared/types/` and `shared/utils/` are auto-imported on both sides. Since Nuxt 4.3 the `#server` alias also lets deeply nested handlers import from anywhere in `server/` without long relative paths. ## Mistakes interviewers listen for - Creating `app/server/` because everything else moved into `app/`. Nuxt 4 still resolves the server directory as `<rootDir>/server`, so nothing in `app/server/` is registered as a route. - Treating `server/api` as private to the app. It is an ordinary HTTP endpoint; any client that knows the URL can call it. - Putting a sitemap or a webhook in `server/api` and then publishing an `/api/...` URL to the outside world. - Leaving the method suffix off a mutating route, so `reserve.ts` answers GET as well as POST. - Expecting every page-routing feature in server routes. Nuxt's docs note that server routes do not support the full dynamic-route functionality of `app/pages`, so keep server paths simple.

  • Can you change the /api prefix for files in server/api?
    Yes. Nitro's `apiBaseURL` option, set under the `nitro` key in `nuxt.config.ts`, replaces the `/api` prefix. One side effect: Nuxt's check for JSON requests looks for paths starting with `/api/`, so after a rename those handlers get a JSON error body only when the request signals JSON, for example through its `Accept` header.
  • How do you share a TypeScript type between a server route and a page in Nuxt 4?
    Put it in the root `shared/` directory. Files in `shared/types/` and `shared/utils/` are auto-imported in both the Vue app and the server. Importing from `app/` inside `server/`, or the reverse, is what Nuxt's docs warn against, because the two are built into separate bundles with different auto-imports.
  • What do the .dev, .prod and .prerender suffixes on a server route file do?
    They are environment suffixes that Nitro reads after the optional method suffix. `server/api/debug.get.dev.ts` registers `GET /api/debug` in the dev server only; `.prod` limits a handler to production builds and `.prerender` to the prerender pass. They let you ship a debugging endpoint without it reaching production.

saying these in an interview costs you the question

  • In Nuxt 4 the server directory moved into app/ with the pages and components.
  • Handlers in server/routes need a different helper from defineEventHandler.
  • Nitro keeps the full file name, so sitemap.xml.ts serves /sitemap.xml.ts.
  • Handlers in server/api can only be called by the app's own pages.
  • A handler without a method suffix only answers GET requests.
  • The /api prefix changes only the URL; thrown errors come back in the same format everywhere.
open as a page

In a Nuxt 4 server route, how do you read the path parameter, query and JSON body, and reject bad input?

level: middleimportance: must knowfreq 40%

basics

~20 s

Use getRouterParam for the path parameter, getQuery for the query (values arrive as strings) and await readBody for the body, or their validated variants that answer 400. Reject input by throwing createError with status and statusText.

open as a page

In Nuxt 4, an inventory proxy is wrapped in `defineCachedEventHandler(handler, { maxAge: 60 })`. What happens on a miss, on a hit, and after 60 seconds?

level: seniorimportance: should knowfreq 33%

basics

~20 s

A miss runs the handler and stores its response in Nitro's cache storage; a hit replays it without calling upstream. After 60 seconds the default swr: true serves the stale response while a background refresh replaces it.

open as a page

In Nuxt 4, server middleware sets `event.context.warehouse` from a cookie and a `defineCachedEventHandler` route reads it. Why does every shopper then see the first shopper's stock, and how do you fix it?

level: seniorimportance: should knowfreq 24%

basics

~20 s

The cached handler's key comes from the URL and varies headers only, while event.context passes through unkeyed, so the first shopper's warehouse is cached for everyone. Put the warehouse into the key, or cache just the upstream call per warehouse.

open as a page

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?

level: seniorimportance: should knowfreq 29%

basics

~20 s

Server 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.

open as a page

In Nuxt 4, what does Nitro's `useStorage()` give a server route, and where does that data live in development versus a production deployment?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

useStorage() returns Nitro's key-value storage, with getItem, setItem, removeItem and getKeys over named mount points. In development the data and cache mounts are folders on disk; in production anything unconfigured is in memory, except the data mount on Node presets.

open as a page