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?
answer
- one folder adds a prefix
- server/ stays beside app/
- brackets become parameters
- a verb before the extension
- JSON or HTML when it throws
basics
~20 sBoth 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 sIn 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// 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
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.
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.
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.
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.