Spots

Nuxt Server Routes Explained: How Nitro Builds Your API

Open any Nuxt project and there's a good chance a server/api folder is already sitting in it — a hello.ts here, a login.post.ts there. Ask most people what it is and the answer is usually "the API routes." Ask what actually runs those files, in what order, and whether they can use the same useState or useRoute composables as the rest of the app, and the answers get much shakier. That gap is where the interesting bugs live: a middleware that silently runs before the one it's supposed to follow, an event handler that dies with useState is not defined for no obvious reason, a readBody() that comes back empty.

This article is written against Nuxt 4.x (verified

This article is written against Nuxt 4.x (verified against the v4.5 release line, August 2026 — Nuxt 3 reached end-of-life on July 31, 2026). The directory names below assume the server/ layout, which — unlike pages/, components/, and the rest of your app code — stays at the project root in both Nuxt 3 and Nuxt 4's new app/-nested structure. If you've read the hydration mismatch episode or the useState vs ref episode, this one picks up the other half of "what runs where": not the Vue app rendering twice, but the completely separate server world sitting next to it.

By the end of this article you'll be

By the end of this article you'll be able to: Explain what Nitro and server/api/server/routes/server/middleware actually are, and how a filename becomes a route Read and write request data correctly with getQuery, getRouterParam, and readBody Predict the real execution order of your server middleware — not the order you assume Explain why Vue composables like useState don't work inside a server route, and what to use instead Know when calling your own API route with useFetch during SSR involves the network at all

You've built Nuxt pages and components, and you've

You've built Nuxt pages and components, and you've probably already dropped a file into server/api and had it work. You don't need prior backend framework experience — this article treats Nitro as its own subject, not "Express with different syntax." The problem: it looks like the rest of your app, but it isn't

Say you want a small /api/profile endpoint that

Say you want a small /api/profile endpoint that returns the current user, and — since you already have a useState('user') that holds the logged-in user everywhere else in the app — reusing it here feels natural:

Run it, and instead of JSON you get

Run it, and instead of JSON you get a 500: useState is not defined. It's the exact composable you use in every .vue file, so it reads like a missing import — but it isn't one. server/ gets its own auto-imports (h3 helpers, Nitro utilities, server/utils), and Vue composables aren't among them; import it from #app explicitly and the build refuses with "Vue app aliases are not allowed in server runtime." The problem is where it's being called from. useState, useRoute, useFetch — the whole family of Nuxt composables — depend on there being a current Nuxt application instance to attach to. A server/api file has no such thing. It's not part of the Vue app at all; it's a plain request handler that Nitro invokes directly, with nothing Vue-shaped anywhere near it.

This is the trap the "Nuxt is just

This is the trap the "Nuxt is just Vue with routing" mental model sets. server/api looks like it belongs to the same app as your pages because it lives in the same repo, ships in the same deploy, and even shares the same nuxt dev process — but it's a different runtime with a different lifecycle, and the rules that make composables work don't apply there. The mental model: two runtimes, one project A Nuxt project is really two separate request-handling worlds glued together at build time:

The Vue/app world. Pages, components, layouts, and composables

The Vue/app world. Pages, components, layouts, and composables. Every request for a page spins up a Nuxt application instance (server-side, then again client-side for hydration), and that instance is what useState, useRoute, and friends attach themselves to. This is the world the last two episodes of this series lived in.

The Nitro/h3 world. server/api, server/routes, and server/middleware. Nitro

The Nitro/h3 world. server/api, server/routes, and server/middleware. Nitro is the server engine Nuxt is built on — it's what starts the process, decides which file handles which URL, and runs each matched file as a plain function that receives an H3Event (from h3, the tiny HTTP toolkit Nitro is built around) and returns a value. There is no component tree here, no "current instance," nothing for a Vue composable to hook into. It doesn't know or care that a Vue app exists elsewhere in the same process.

The two worlds do talk to each other

The two worlds do talk to each other, but only across an explicit boundary: a page calls useFetch('/api/profile') or $fetch('/api/profile'), which sends a request that Nitro routes to your server/api/profile.get.ts handler exactly like it would route a request from curl or a browser tab. The response crosses back as plain, serializable data — never a live object, never a shared reference, never a ref. Whatever you build inside a server route has to assume it's talking to some client, not sharing memory with one.

News

Nuxt Server Routes Explained: How Nitro Builds Your API

Open any Nuxt project and there's a good chance a server/api folder is already sitting in it — a hello.ts here, a login.post.ts there.

@spots #dev
Source: Dev.to
See more like this