Nuxt SSR and Nitro: Keeping the Server Boundary Clear

Nuxt combines routing, SSR, static generation, and a server engine for Vue applications. Nuxt 4 landed this past July, and the integrated setup is convenient enough that unrelated responsibilities can easily end up in the same project. SSR data fetching and the Nitro server layer are the two places where I have seen that happen most often.

SSR, static, hybrid: decide how each route should behave first

Nuxt isn’t SSR-only. Within one project, a marketing page can be prerendered to plain static and dropped on a CDN edge; a dashboard that needs live data runs SSR; some near-static content can be cached and regenerated on an interval. That per-route arrangement is hybrid rendering, specified one rule at a time through routeRules, and it’s the part of Nuxt actually worth the time to understand. Most people’s performance problems aren’t a slow framework — they’re a whole site set to SSR with no thought, recomputing pages on the server every time that could have been static.

Data fetching needs a clear model of how useAsyncData and useFetch behave. The first request runs on the server, serialises its data into the HTML, and sends both to the browser. During hydration, the client reuses that payload instead of fetching it again.

Two traps come up most. One is the double fetch: you use useFetch in a component, then go and $fetch the same data again in some onMounted or watcher — the server fetches once, the client fetches again, and the user pays for a wasted round trip they can watch happen. The other, more dangerous, is leakage. A useAsyncData handler does run on the server, but written carelessly it gets carried to the client too. If that callback reads an API key straight out of the environment, or dials an internal service address directly, and the logic isn’t properly fenced into server-only territory, it can end up bundled into what ships to the browser. You can’t blame Nuxt for this — it’s you writing server-only things onto a path that hydrates.

The rule of thumb is simple: can the source of that data see daylight? If it can — a public API, JSON on a CDN — put it wherever. If it can’t — internal addresses, secrets, privileged queries — it stays on the server, and the front end only ever receives the already-trimmed result.

Nitro as a BFF is smooth, but smooth isn’t a boundary

Nitro is the server engine underneath Nuxt; the server routes you create under server/api/ run on it. It can do more than people assume: the same layer is both the SSR data source and a lightweight BFF gateway. When the front end wants data, it doesn’t call a third party directly — it calls its own Nitro route, and that layer reaches downstream, adds the auth, and decides the response shape. External requests never touch the data layer directly. I’ve run this setup, and it’s genuinely convenient: secrets stay on the server, the call chain is centralised, and the front end only handles clean data.

Nitro can do the job easily, which also makes the boundary easy to blur. Its server layer can see browser-facing aggregation and backend-facing risk, and both may end up in the same directory under the same kind of defineEventHandler. A reviewer then has to distinguish routes that trim data for the front end from routes that carry internal-service access. Mixing “assemble the fields the lobby needs” with “query the table carrying DB credentials directly” in one server/api/ folder hides where the risk lands.

I still lean towards a separate BFF. Using Nitro in that role is reasonable for a small project and saves one service; as the system grows, I move the backend-facing, risk-carrying work out and leave Nitro with SSR and lightweight front-end aggregation. The split makes the browser and backend boundaries visible in the architecture instead of leaving them to whichever file was handiest at the time.

Nuxt makes a lot of things simple, and that’s its worth. But simple and bounded are two different things. It makes it possible to cram aggregation, secrets, and downstream access all into a server route; that doesn’t make it advisable. Convenience is a gift the tool hands you; the boundary is a discipline you keep yourself. A green build has never once meant a sound design.

Notes

  • Nuxt’s hybrid rendering uses routeRules to set SSR / static / cache per route; most performance problems come from setting the whole site to SSR with no thought.
  • useAsyncData / useFetch fetch on the server first and hand off to the client after hydration; get it wrong and you double fetch or carry a server-only secret into the client bundle.
  • Deciding where data belongs is one question: can its source see daylight? If not, it stays on the server.
  • A Nitro server route can be both the SSR source and a lightweight BFF, with external requests never touching the data layer directly; a convenient setup.
  • But convenience shouldn’t draw architectural boundaries; once a system grows, pull the backend-facing, risk-carrying layer out rather than mixing it with browser-facing aggregation in one folder.

Sheng’s take, drafted with Claude · part of the 2026-06-13 blog renovation, paint still drying.