All topics
Frontend · Learning hub

Nuxt notes for developers

Master Nuxt with a curated set of 1 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallTest yourself — Nuxt quizMore Frontend notes
Nuxt

Nuxt 3 Essentials

Nuxt 3 Essentials Nuxt is Vue's meta-framework — the Next.js equivalent for the Vue ecosystem. Nuxt 3 rebuilt the framework on Vite and a new server engine call

Nuxt 3 Essentials

Nuxt is Vue's meta-framework — the Next.js equivalent for the Vue ecosystem. Nuxt 3 rebuilt the framework on Vite and a new server engine called Nitro, adding universal rendering (SSR/SSG/hybrid), file-based routing, auto-imports, and a composables-first data-fetching model. If you're comfortable with Vue's Composition API, most of Nuxt is 'the same Vue, with conventions and a server layer bolted on' rather than a separate mental model to learn.

Project Structure & Auto-Imports

Nuxt scans specific directories and wires them up automatically — no manual imports for components, composables, or Vue APIs like `ref`/`computed`. This is convention over configuration: `pages/` becomes routes, `components/` are globally available, `composables/` and `utils/` are auto-imported anywhere.

my-nuxt-app/
  pages/
    index.vue          # /
    about.vue           # /about
    posts/
      [slug].vue         # /posts/:slug (dynamic segment)
      index.vue          # /posts
  components/
    AppHeader.vue        # auto-imported as <AppHeader />
    PostCard.vue
  composables/
    useCart.ts            # auto-imported as useCart()
  server/
    api/
      posts.get.ts        # GET /api/posts
      posts/[id].delete.ts # DELETE /api/posts/:id
    middleware/
      logger.ts
  layouts/
    default.vue
    admin.vue
  middleware/
    auth.ts               # route middleware, usable via definePageMeta
  app.vue                 # root component
  nuxt.config.ts
<!-- pages/posts/[slug].vue -->
<script setup lang="ts">
// `ref`, `computed`, `useRoute`, `useFetch` are all auto-imported — no import lines needed
const route = useRoute()
const slug = route.params.slug as string

const { data: post, error } = await useAsyncData(`post-${slug}`, () =>
  $fetch(`/api/posts/${slug}`),
)

if (error.value) {
  throw createError({ statusCode: 404, statusMessage: 'Post not found' })
}

useSeoMeta({
  title: post.value?.title,
  description: post.value?.excerpt,
})
</script>

<template>
  <article v-if="post">
    <h1>{{ post.title }}</h1>
    <div v-html="post.content" />
  </article>
</template>

Data Fetching: useFetch & useAsyncData

`useFetch` and `useAsyncData` are SSR-aware: they run on the server during server rendering and serialize the result into the HTML payload, so the client doesn't re-fetch on hydration — this is the main reason to prefer them over a plain `onMounted` + `fetch`. `useFetch(url)` is sugar over `useAsyncData` + `$fetch` for the common case; reach for `useAsyncData` directly when the fetching logic is more than a single URL call.

<script setup lang="ts">
// useFetch — shorthand for a single endpoint
const { data: posts, pending, error, refresh } = await useFetch('/api/posts', {
  query: { limit: 20 },
  key: 'posts-list',       // dedupe/cache key — defaults to a hash of url+options
  server: true,             // fetch during SSR (default)
  lazy: false,               // false = block navigation until resolved (default)
  default: () => [],         // value while pending, before first resolve
})

// useAsyncData — for composed/multi-step fetching logic
const { data: dashboard } = await useAsyncData('dashboard', async () => {
  const [user, stats] = await Promise.all([
    $fetch('/api/user'),
    $fetch('/api/stats'),
  ])
  return { user, stats }
})

// lazy: true — don't block navigation, show a loading state instead
const { data: comments, pending: commentsPending } = useFetch('/api/comments', {
  lazy: true,
})
</script>

<template>
  <div v-if="pending">Loading…</div>
  <ul v-else>
    <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
  </ul>
  <button @click="refresh()">Reload</button>
</template>

Use `$fetch` directly (not `useFetch`) for one-off calls that don't need to run during SSR — form submissions, button-triggered actions — since it skips the extra reactivity/caching machinery you don't need there.

Nitro Server Routes (server/api)

Nitro is Nuxt's server engine — it powers universal deployment (Node server, serverless functions, edge workers, static hosting) from the same codebase, and gives you a full backend inside `server/`. File naming conventions map to HTTP methods and routes automatically.

// server/api/posts.get.ts — GET /api/posts
export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  const limit = Number(query.limit ?? 20)
  const posts = await db.post.findMany({ take: limit })
  return posts
})

// server/api/posts.post.ts — POST /api/posts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  if (!body.title) {
    throw createError({ statusCode: 400, statusMessage: 'title is required' })
  }
  return await db.post.create({ data: body })
})

// server/api/posts/[id].delete.ts — DELETE /api/posts/:id
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  await db.post.delete({ where: { id } })
  setResponseStatus(event, 204)
})

// server/middleware/logger.ts — runs on every request
export default defineEventHandler((event) => {
  console.log(`[${event.node.req.method}] ${event.node.req.url}`)
})

// server/utils/db.ts — auto-imported inside server/, like composables for the client
export const db = createDbClient(useRuntimeConfig().databaseUrl)

Rendering Modes

Nuxt configures rendering per-route in `nuxt.config.ts`, mixing SSR, static prerendering, and SPA-only pages in a single app — this is what 'hybrid rendering' means in Nuxt.

// nuxt.config.ts
export default defineNuxtConfig({
  // Global default: universal rendering (SSR on first load, SPA-like navigation after)
  ssr: true,

  routeRules: {
    '/': { prerender: true },                    // SSG at build time
    '/blog/**': { swr: 3600 },                    // stale-while-revalidate, cached 1h
    '/dashboard/**': { ssr: false },               // client-only rendering (SPA)
    '/admin/**': { index: false },                 // exclude from search engines
    '/api/legacy/**': { proxy: 'https://old-api.example.com/**' },
  },

  nitro: {
    preset: 'vercel', // or 'node-server', 'cloudflare-pages', 'netlify', etc.
  },
})

// nuxt generate — full static build (every page prerendered, deployable as static files)
// nuxt build  — universal build (SSR server + hybrid route rules)
// nuxt dev    — dev server with HMR
  • Universal (SSR) — default. Server renders HTML on first request, client hydrates and takes over navigation.

  • Static (SSG) — `nuxt generate` or `routeRules` with `prerender: true`; every page built to static HTML ahead of time.

  • SPA — `ssr: false` per-route; client-only rendering, good for authenticated dashboards where SEO doesn't matter.

  • Hybrid — different `routeRules` per path in the same app, the main advantage of Nuxt 3's rendering model over an all-or-nothing choice.

Common Gotchas

  • Fetching in onMounted instead of useFetch — skips SSR entirely and causes a client-side waterfall/loading flash; use `useFetch`/`useAsyncData` so data is in the initial HTML.

  • Accessing browser-only APIs during SSR — `window`/`document` don't exist on the server; guard with `import.meta.client` (or the older `process.client`) or move the code into a `onMounted` hook.

  • Duplicate fetches from mismatched keys — `useFetch` calls with the same auto-generated key dedupe; giving two different calls the same explicit `key` unintentionally makes them share (and overwrite) cached state.

  • Putting secrets in runtimeConfig's public keys — only values under `runtimeConfig.public` are exposed to the client bundle; API keys/secrets belong at the top level of `runtimeConfig`, server-only.

  • Forgetting definePageMeta runs at compile time — its options (layout, middleware, keepalive) are statically extracted, so you can't reference runtime variables inside it.

Keep your Nuxt knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever