Nuxt — SSR, SSG, ISR, Server Routes
TL;DR
Nuxt 3 is to Vue what Next.js is to React — the full-stack meta-framework. It gives you file-based routing, multiple rendering modes (SSR, SSG, ISR, SPA, hybrid per-route), server routes (server/api/), auto-imports, and a rich module ecosystem. Senior topics: when to pick each rendering mode, how data fetching interacts with hydration, useFetch/useAsyncData vs $fetch, server-only vs universal code, and the App Router-equivalent layers + components subdirectories that scale across teams.
Interview Q&A
Q: Rendering modes — what’s the trade-off?
A: Set per-route or app-wide via nitro.routeRules or defineNuxtConfig({ routeRules }):
| Mode | When |
|---|---|
| SSR (universal) | dynamic per-request HTML; fresh data, slower TTFB than static |
| SSG (prerendered) | static HTML built at build time; fastest, requires re-build for changes |
| ISR (incremental) | static + revalidate every N seconds; “static-ish” with freshness |
| SWR | serve stale-while-revalidate at the edge |
| SPA (client-only) | no SSR; HTML is a shell, JS renders everything; admin panels behind auth |
| Hybrid | per-route mix; marketing pages SSG, dashboard SPA, blog posts ISR |
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
"/": { prerender: true }, // SSG
"/blog/**": { isr: 60 }, // ISR — revalidate every 60s
"/dashboard/**": { ssr: false }, // SPA
"/api/**": { cors: true, headers: { "x-frame": "deny" } },
},
});
The decision flow: default to SSR for most pages, SSG for marketing/landing, SPA for authed admin where SEO doesn’t matter, ISR for content that changes occasionally.
Q: useFetch vs useAsyncData vs $fetch — when each?
A:
| Where it runs | Reactive? | Use for | |
|---|---|---|---|
useFetch(url) |
both SSR and client | yes — { data, pending, error, refresh } |
most data fetching; convenient wrapper around useAsyncData + $fetch |
useAsyncData(key, fn) |
both SSR and client | yes | when fetching needs custom logic (multi-source, computed body) |
$fetch(url) |
wherever called (raw) | no — returns a Promise | event handlers, mutations, server routes |
<script setup>
// Page-level fetch — SSR-aware, no waterfall
const { data: post, pending, error, refresh } = await useFetch(`/api/posts/${id}`);
// Custom logic with useAsyncData
const { data: feed } = await useAsyncData("feed", async () => {
const [posts, ads] = await Promise.all([
$fetch("/api/posts"),
$fetch("/api/ads"),
]);
return { posts, ads };
});
// Inside a handler — just $fetch
async function submit() {
await $fetch("/api/posts", { method: "POST", body: form.value });
await refresh(); // re-run the useFetch above
}
</script>
Key: useFetch/useAsyncData are SSR-aware — fetched on the server, serialized into the HTML, rehydrated on the client (no double-fetch). $fetch is raw — use for handlers and inside server routes.
Q: Server routes (server/api/).
A: Nuxt 3 ships with Nitro, a server engine. Files under server/api/ become endpoints:
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id");
const user = await db.user.findUnique({ where: { id: Number(id) } });
if (!user) throw createError({ statusCode: 404 });
return user;
});
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event);
return db.user.create({ data: body });
});
These run on the server only; you can use Node APIs, DB clients, etc. The client’s $fetch("/api/users/1") hits them. Routing follows Nitro’s conventions ([id] for params, method-suffix .get.ts/.post.ts).
Q: Auto-imports — what gets imported automatically?
A: Nuxt auto-imports:
- All Vue Composition API (
ref,computed,watch,onMounted, etc.). - Nuxt composables (
useFetch,useRoute,useRouter,useState,useCookie,useRuntimeConfig). - Components in
components/(the file name = the component name). - Composables in
composables/(the function name). - Utils in
utils/.
You can use them without import statements. Trade-off: less explicit; needs IDE tooling to “go to definition” work. Most teams accept the trade-off; some disable with imports: { autoImport: false } in nuxt.config.
Q: Server-only vs universal code.
A: Code in server/ runs only on the server. Code anywhere else (pages/, components/, composables/) runs on both server (during SSR) and client (during interaction).
For server-only logic in a universal file, guard with import.meta.server:
if (import.meta.server) {
const fs = await import("fs/promises");
// ...
}
if (import.meta.client) {
window.addEventListener("focus", ...);
}
Code that imports Node-only modules at the top level breaks the client build — that’s the most common SSR gotcha for Nuxt newcomers.
Q: useState — what’s it for?
A: Nuxt’s SSR-friendly state primitive — a ref whose value is serialized between server and client, and shared across components by key. Lightweight alternative to Pinia for simple cross-component state.
const counter = useState("counter", () => 0);
counter.value++;
If you call useState("counter") in two components, they share the same value. For complex state with actions, use Pinia.
Q: Layouts and pages.
A: pages/ directory = file-based routing. [id].vue for params, [...slug].vue for catch-all, (group)/ for groups without affecting URL.
layouts/ directory = persistent layouts. Default default.vue; override per-page:
<!-- pages/admin.vue -->
<script setup>
definePageMeta({ layout: "admin" });
</script>
layouts/admin.vue wraps the page with the admin chrome.
Q: Middleware.
A: Per-route logic that runs before navigation:
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const user = useUserStore();
if (!user.isAuthenticated) return navigateTo("/login");
});
<script setup>
definePageMeta({ middleware: ["auth"] });
</script>
Plus server middleware (server/middleware/) for request-level concerns (CORS headers, logging) and global middleware (middleware/*.global.ts).
Q: Modules — how does the ecosystem extend Nuxt?
A: Modules are first-class. Install + add to nuxt.config.ts:
export default defineNuxtConfig({
modules: [
"@pinia/nuxt",
"@nuxtjs/tailwindcss",
"@vueuse/nuxt",
"@nuxtjs/i18n",
"@nuxt/image",
],
});
Modules can add server routes, auto-imports, components, runtime config — they hook into the build/render pipeline. The ecosystem is the main reason to choose Nuxt over hand-rolled Vue SSR.
Q: Differences from Next.js (React).
A:
| Nuxt 3 | Next.js (App Router) | |
|---|---|---|
| Routing | pages/ (default) or app/ (Nuxt 4) |
app/ |
| Data fetching | useFetch / useAsyncData |
fetch + RSC, loader-style |
| Server components | universal (server/api); no RSC equivalent |
RSC native |
| Rendering modes | route rules (SSR/SSG/ISR/SPA per route) | per-segment via dynamic / revalidate |
| Server engine | Nitro | Next’s own |
| Modules ecosystem | first-class | community packages |
Nuxt’s per-route render mode rules are arguably cleaner than Next’s per-segment options. Next’s RSC is a bigger architectural innovation that Nuxt hasn’t matched.
Gotchas / edge cases
- Importing Node-only modules at top level in universal files breaks client builds. Use dynamic import +
import.meta.serverguard. useFetchdoesn’t dedupe across components with different URLs but same data — use a shareduseAsyncData("key", ...)with a stable key.- Hydration mismatches — server renders one HTML, client tries to render different — Vue warns. Common causes: rendering
Date.now()orMath.random()server-side, branching onwindow/documentwithoutimport.meta.clientguard. useStatevalue persists across pages — that’s the feature, but it also means you can leak state. Reset explicitly on logout or route change if needed.- ISR revalidation in dev doesn’t behave the same as production — test with
nuxt build && nuxt preview. - Server routes are not cached by default — set
cacheroute rule or use Nitro’scachedEventHandler.
What a senior is expected to say
- “Nuxt 3 = Vue’s Next.js. File-based routing, Nitro server engine, per-route render modes via
routeRules— SSR by default, SSG for static, ISR for occasionally-changing, SPA for authed.” - “
useFetch/useAsyncDataare SSR-aware — data fetched on the server, serialized into HTML, no double-fetch on hydration.$fetchis the raw HTTP utility for handlers and server routes.” - “Server routes in
server/api/*run only on the server via Nitro — Node APIs, DB clients, secrets are safe there.” - “Auto-imports are convenient but you trade explicitness for them; modern IDE tooling makes it survivable.”
- “Hydration mismatches are the #1 SSR bug class — guard browser-only code with
import.meta.clientand don’t render time/random values without keying.”
Cross-references
- Rendering modes (general): ../19_rendering_modes/
- Pinia for client state: 10_pinia.md
- Component-level data fetching patterns: ../11_apis_data_fetching/
Further reading
- Nuxt docs: https://nuxt.com/docs
- Nitro: https://nitro.unjs.io/
- Route rules: https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering