Guide

Nuxt fundamentals explained

A logistics team needs a cargo tracker that loads shipment status on first paint for SEO and warehouse tablets, then updates live as containers move through ports — without maintaining separate Vue SPA and Node API repositories. Nuxt 3 is the full-stack framework for Vue.js: file-based pages, layouts, and middleware; universal data fetching with useFetch and useAsyncData; server API routes powered by the Nitro engine; and configurable SSR, SSG, or SPA rendering from one codebase. Nuxt auto-imports components, composables, and Vue APIs so you write less boilerplate than a hand-rolled Vite + Vue Router stack, while staying closer to Vue idioms than Next.js is to React. This guide covers Nuxt’s directory conventions, data-fetching lifecycle, Nitro server handlers, modules and deployment targets, a Harbor Fleet cargo tracker worked example, a framework decision table, common pitfalls, and a production checklist.

What Nuxt is (and how it extends Vue)

Vue is a UI library. Nuxt is a meta-framework that wires Vue into a complete application shell:

  • Routing — files in pages/ become routes automatically (like Next.js App Router or Remix file routes).
  • Rendering — the same components render on the server for HTML-first delivery or pre-render at build time.
  • Data layeruseFetch runs on server during SSR and hydrates on the client without duplicate requests.
  • Backendserver/api/ and server/routes/ define HTTP handlers compiled by Nitro to Node, serverless, or edge workers.
  • Tooling — Vite for dev HMR, TypeScript path aliases, and a module ecosystem for auth, CMS, and image optimization.

Nuxt 3 (current major version) replaced the webpack-based Nuxt 2 build with Vite and Nitro. If you know Vue 3 Composition API and <script setup>, Nuxt adds conventions — not a second component model. Teams already on plain Vue + Vite often adopt Nuxt when they need SSR, API colocation, or SEO without assembling five libraries manually.

Core directories

Directory Purpose
pages/ File-based routes; pages/shipments/[id].vue maps to /shipments/:id
layouts/ Wrapper templates with <slot />; pages set definePageMeta({ layout: 'admin' })
components/ Auto-imported Vue SFCs; nested folders become prefixed names (UiButton.vue<UiButton>)
composables/ Auto-imported use* functions shared across pages
server/api/ Nitro event handlers; server/api/health.get.tsGET /api/health
middleware/ Route guards run before navigation (auth, locale redirects)
plugins/ Run at app init (inject clients, register directives)
nuxt.config.ts Modules, runtime config, route rules, Nitro preset, CSS, and build hooks

Rendering modes and route rules

Nuxt defaults to universal SSR: the server renders HTML with data, the client hydrates and takes over navigation. You can override per route or globally:

  • SSR (default) — dynamic HTML per request; best for authenticated dashboards and frequently changing data.
  • SSG / prerendernuxt generate or routeRules: { '/blog/**': { prerender: true } } emits static HTML at build time; ideal for marketing and docs.
  • SPAssr: false in config ships a client-only bundle; use when SEO does not matter and deploy is purely static CDN.
  • ISR / SWRrouteRules with swr: 3600 caches server responses and revalidates in the background (Nitro storage or CDN integration).
  • Hybrid — public catalog pages prerendered, /account/** SSR with cookies; one app, different rules per path prefix.

routeRules in nuxt.config.ts is the modern replacement for scattering generate.routes lists. Pair prerendered marketing pages with SSR app shells the way teams split Next.js marketing vs app routes — but without two repositories.

Data fetching: useFetch and useAsyncData

The most common Nuxt mistake is calling fetch in onMounted after SSR already ran empty. Nuxt’s composables coordinate server and client:

  • useFetch('/api/shipments') — wraps $fetch (ofetch); deduplicates identical keys; serializes result into the HTML payload for hydration.
  • useAsyncData('shipments', () => $fetch(...)) — lower-level; you supply a stable key and async function; use when transforming data or combining multiple sources.
  • $fetch — universal fetch helper; on server can call internal Nitro routes directly without HTTP loopback.
  • refresh() / clear() — returned from composables to re-run queries after mutations.

During SSR, useFetch awaits on the server and embeds JSON in window.__NUXT__ (payload). On client navigation, Nuxt reuses cached data when the key matches and optionally revalidates. Set lazy: true to render the page shell immediately and load data after mount — similar to React Suspense boundaries.

When to fetch where

Pattern Use when
useFetch in <script setup> Page needs data for SEO or first paint; runs on server + client
server/api handler + useFetch('/api/...') Hide database credentials; single BFF layer for the Vue app
$fetch in event handler only Server route proxies third-party API with secret keys
Client-only onMounted fetch User-specific widgets that must not leak into SSR HTML (rare)

Nitro server routes and runtime config

Nitro is Nuxt’s server engine. Files in server/ compile to a standalone server or serverless functions:

// server/api/shipments/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  const row = await db.shipment.findUnique({ where: { id } })
  if (!row) throw createError({ statusCode: 404, message: 'Not found' })
  return row
})

Handlers use defineEventHandler with Web Standard event objects. HTTP method suffixes (.get.ts, .post.ts) map to verbs. server/middleware/ runs on every request (logging, CORS). server/plugins/ hooks Nitro startup (database connection pools).

Runtime config separates public from private env vars:

  • runtimeConfig.public.apiBase — exposed to client via useRuntimeConfig()
  • runtimeConfig.dbUrl — server-only; never serialized to the browser

Map NUXT_DB_URL and NUXT_PUBLIC_API_BASE environment variables at deploy time. Nitro presets target Node (node-server), Vercel, Netlify, Cloudflare Workers, Deno, and static hosting with optional serverless API routes.

Modules, middleware, and auto-imports

Nuxt modules extend config at build time. Common choices:

  • @nuxtjs/tailwindcss — Tailwind v4 integration; pairs with our Tailwind fundamentals guide.
  • @pinia/nuxt — client state stores; use for UI toggles, not server data cache.
  • @nuxt/image — responsive images with lazy loading and CDN providers.
  • @nuxt/content — Markdown/MDC content layer for docs and blogs.
  • nuxt-auth-utils or custom session middleware — cookie sessions on Nitro.

Route middleware (middleware/auth.ts) runs before page navigation. Return navigateTo('/login') to redirect. definePageMeta({ middleware: 'auth' }) applies per page. Server middleware differs: it guards API routes before handlers execute.

Auto-imports mean ref, computed, useRoute, and your composables/useTracking.ts need no import statements. Disable selectively if tree-shaking surprises you, but most teams embrace the ergonomics.

Worked example: Harbor Fleet cargo tracker

Harbor Fleet ships a customer-facing cargo tracker on Nuxt 3 with the Node Nitro preset behind nginx. The design optimizes for SEO on public tracking pages and fast CSR navigations inside the ops dashboard:

  • Route tree: pages/index.vue (marketing hero) → pages/track/[bl].vue (bill-of-lading lookup, SSR) → pages/dashboard/shipments/index.vue (auth-gated list) → pages/dashboard/shipments/[id].vue (detail + timeline).
  • Public track page: useFetch(`/api/public/track/${bl}`) runs on server; meta tags set via useSeoMeta({ title: () => `Shipment ${bl}` }) for Google snippets.
  • API layer: server/api/public/track/[bl].get.ts queries read replica; strips internal cost fields before JSON response.
  • Dashboard auth: middleware/auth.ts reads sealed session cookie; server middleware on /api/dashboard/** double-checks role claims.
  • Live updates: detail page opens EventSource to /api/dashboard/shipments/[id]/events (Nitro streaming) for milestone pushes; falls back to 30s polling.
  • Rendering rules: routeRules: { '/': { prerender: true }, '/track/**': { swr: 60 }, '/dashboard/**': { ssr: true } }.
  • Mutations: status notes POST to server/api/dashboard/shipments/[id]/notes.post.ts; client calls refresh() on the detail useAsyncData key after success.

Tests call Nitro handlers with await $fetch('/api/...', { headers }) in Vitest without launching a browser. One Playwright path covers BL lookup end-to-end. The pattern mirrors our Remix guide (loaders + actions) but stays in Vue templates and composables throughout.

Framework decision table

Choose Nuxt when… Prefer Next.js when… Prefer plain Vue + Vite when…
Your team already ships Vue components React ecosystem and RSC patterns are the default App is a small SPA behind login with no SEO
You want file-based routing + SSR without manual wiring Vercel-first deployment and Image Optimization are requirements You embed Vue islands into Laravel/Rails via Inertia
Colocated Nitro API routes replace a separate Express service Large Contentful/headless CMS + App Router integrations exist Backend is a separate team-owned OpenAPI service only
Hybrid SSG marketing + SSR app shell in one repo Edge middleware auth on Vercel is critical path Bundle must stay minimal and you control every dependency
Vue templates and reactivity model fit the product Hiring pool favors React/Next on job boards Prototype phase; migrate to Nuxt when SSR need appears

Versus Remix: both colocate server data with UI. Nuxt centers Vue and Nitro; Remix centers React and Web Standard loaders/actions. Versus SvelteKit: similar meta-framework shape; choose based on team language preference and component library availability.

Common pitfalls

  • Fetching in onMounted for SSR pages — users see empty shell then flash; use useFetch or useAsyncData at top level.
  • Exposing secrets via runtimeConfig — only keys under public are safe; database URLs belong in private config accessed server-side only.
  • Non-unique useAsyncData keys — colliding keys share cache across unrelated pages; include route params in the key string.
  • Importing server-only modules in components — bundles blow up or leak secrets; keep db imports in server/ handlers only.
  • Disabling SSR globally for one client bugssr: false trades away SEO and first paint; fix hydration mismatch instead.
  • Pinia as API cache — duplicates useFetch invalidation; use Pinia for UI state, composables for remote data.
  • Ignoring payload size — serializing huge lists into SSR payload slows TTFB; paginate server responses.

Production checklist

  • Scaffold with npx nuxi@latest init; enable TypeScript and ESLint module from day one.
  • Define routeRules per section (prerender, SSR, SWR) instead of one global mode.
  • All initial page data via useFetch/useAsyncData; mutations call refresh() or optimistic local state.
  • Server handlers validate input with Zod; return createError for 4xx/5xx consistently.
  • Runtime config documented in .env.example; secrets injected at deploy, never committed.
  • Nitro preset matches host (Node vs serverless); health check at /api/health bypasses heavy deps.
  • useSeoMeta and canonical URLs on public routes; test with view-source, not only client render.
  • Vitest tests for composables and server handlers; Playwright smoke on critical flows.
  • nginx or CDN caches static .output/public assets with fingerprinted filenames.
  • Monitor hydration errors in production logging; mismatches indicate SSR/client divergence.

Key takeaways

  • Nuxt 3 unifies Vue 3, Vite, and Nitro into one full-stack framework with file-based conventions.
  • useFetch and useAsyncData bridge SSR and client navigation without duplicate network calls.
  • server/api handlers colocate backend logic; runtime config keeps secrets server-side.
  • routeRules mix prerendered marketing, cached public pages, and dynamic SSR dashboards.
  • Choose Nuxt when Vue is the UI layer and you need SEO, BFF APIs, and hybrid rendering in one repo.

Related reading