Nuxt: Full-Stack Vue

By Syncfusion®, Inc. · Last updated

What Nuxt adds to Vue, creating a Nuxt 4 project and its folders, auto-imports, file-based routing, layouts, useFetch and useAsyncData with keys and payload, server API routes with Nitro, route middleware, useState, SEO with useSeoMeta, runtime config, modules, rendering modes and deployment.

What Nuxt Adds to Vue

  • Nuxt = a full framework built on Vue. It does SSR, SSG and hybrid rendering for you.
  • In the last lesson you wired SSR by hand: two entry files, a server, head tags, state transfer. Nuxt does all of that.
  • It also adds file-based routing, layouts, data fetching helpers and a server for your own API.
  • Your components are still normal Vue: ref, computed, props, slots and Pinia all work.
  • The current version is Nuxt 4. Its server engine is called Nitro.

Simple lesson: Nuxt = Vue + routing + SSR + a server, already wired together.

A Real-World Example

  • Meera wants a public website that Google can read, plus an admin area.
  • Menu pages must come as HTML from the server. The admin area can be a plain SPA.
  • In Nuxt, she makes one file per page, and one file per API endpoint. Nuxt builds the router and the server.
  • The box below imitates that with plain Vue. Each file name becomes a route.
  • What happened: the Console shows each file name and the route made from it.
  • In real Nuxt you never write routes.ts or main.ts. You only add files to pages/.

Nuxt runs on a server, so the boxes in this lesson cannot run Nuxt itself. Each box rebuilds one Nuxt idea with plain Vue, so you can see how it works. The real Nuxt code is shown next to it.

What You Will Learn

  1. When to choose Nuxt, and how to create a project.
  2. The Nuxt 4 folders and auto-imports.
  3. File-based routing, dynamic pages and <NuxtLink>.
  4. Layouts.
  5. useFetch and useAsyncData: keys, status and the payload.
  6. Server API routes with Nitro.
  7. Route middleware and useState.
  8. SEO, runtime config and modules.
  9. Rendering modes, building and deploying.

Creating a Nuxt Project

npm create nuxt@latest meeras-kitchen
cd meeras-kitchen
npm run dev        # http://localhost:3000
npm run build      # server build in .output/
npm run generate   # static site in .output/public/
Folder or fileHolds
app/pages/One .vue file per route
app/components/Components, imported for you
app/composables/use functions, imported for you
app/layouts/Page frames such as header + footer
app/middleware/Route guards
app/plugins/Code that runs when the app starts
app/app.vueThe root component
server/api/Your API endpoints (Nitro)
public/Files served as they are
nuxt.config.tsSettings, modules and route rules
  • Auto-imports: ref, computed, useRoute, your components and composables are available without import lines.
  • TypeScript works out of the box. Nuxt writes the types into a .nuxt/ folder.

Choose Nuxt when you need SEO, fast first loads or your own small API next to the site. For an app that lives behind a login, plain Vue with Vite is often simpler.

Pages, Links and Layouts

  • pages/dish/[id].vue → /dish/:id. Read it with useRoute().params.id.
  • pages/[...slug].vue catches every unknown path, for a 404 page.
  • <NuxtLink to="/menu"> is <RouterLink> plus prefetching of the next page's code.
  • <NuxtPage /> is where pages show, like <RouterView />.
  • A layout wraps pages. layouts/default.vue is used unless a page picks another one.
<!-- app/layouts/default.vue -->
<template>
  <header>Meera's Kitchen</header>
  <slot />
  <footer>Call 98765 43210</footer>
</template>

<!-- app/pages/admin/index.vue -->
<script setup lang="ts">
definePageMeta({ layout: "admin", middleware: "auth" });
</script>

<!-- app/app.vue -->
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
  • What happened: the admin page set meta.layout, so the red frame replaced the green one.
  • That is what <NuxtLayout> and definePageMeta({ layout }) do for you.

Data Fetching: useFetch and useAsyncData

  • useFetch(url) loads data on the server, puts it into the HTML, and sends it to the browser as the payload.
  • In the browser, Nuxt reads the payload, so the request is not made a second time.
  • It returns data, status (idle, pending, success, error), error and refresh().
  • useAsyncData(key, fn) does the same for any async function. The key names the data, so two components share one request.
  • $fetch(url) is a plain request. Use it in event handlers, such as a button that posts an order.
<script setup lang="ts">
const { data: dishes, status, error, refresh } = await useFetch("/api/dishes");
const { data: today } = await useAsyncData("today", () => $fetch("/api/specials"));
</script>
  • What happened: two components asked for the key "dishes", but the Console shows one API request.
  • Refresh made a new request, and both components updated from the same data.

Calling $fetch directly in <script setup> runs it twice: once on the server, once in the browser. Use useFetch or useAsyncData for data the page needs when it loads.

Server API Routes

  • Files in server/api/ become endpoints. The method can be part of the name.
  • server/api/dishes.get.ts → GET /api/dishes. server/api/dishes/[id].get.ts → GET /api/dishes/:id.
  • Each file exports defineEventHandler. Return an object, and Nuxt sends JSON.
  • Read input with getRouterParam, getQuery and readBody. Send errors with createError.
  • This code runs only on the server, so it can use secrets and databases.
// server/api/dishes/[id].get.ts
export default defineEventHandler((event) => {
  const id = Number(getRouterParam(event, "id"));
  const dish = dishes.find((d) => d.id === id);
  if (!dish) throw createError({ statusCode: 404, statusMessage: "Dish not found" });
  return dish;
});

Middleware and Shared State

  • Route middleware = a guard that runs before a page opens. Put it in middleware/auth.ts.
  • Return navigateTo("/login") to redirect. Name it in definePageMeta({ middleware: "auth" }).
  • useState(key, init) = a ref shared by every component, and safe for SSR (a fresh copy per request, sent in the payload).
  • For bigger state, add the @pinia/nuxt module and use Pinia as usual.
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
  const user = useState<string | null>("user", () => null);
  if (!user.value) return navigateTo(`/login?redirect=${to.fullPath}`);
});

SEO, Config and Modules

  • useSeoMeta() sets title and meta tags, typed, on the server and in the browser.
  • runtimeConfig holds settings. Keys under public reach the browser. Others stay on the server, such as API secrets.
  • Modules add features in one line: @pinia/nuxt, @nuxt/image, @nuxtjs/i18n, @nuxt/content.
// app/pages/dish/[id].vue
const { data: dish } = await useFetch(`/api/dishes/${useRoute().params.id}`);
useSeoMeta({
  title: () => `${dish.value?.name} | Meera's Kitchen`,
  description: () => `Order ${dish.value?.name} online.`,
});

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ["@pinia/nuxt", "@nuxt/image"],
  runtimeConfig: {
    paymentSecret: "",                 // server only (NUXT_PAYMENT_SECRET)
    public: { apiBase: "/api" },       // browser too (NUXT_PUBLIC_API_BASE)
  },
});

Rendering and Deploying

  • Default: universal rendering = SSR for the first page, then the app runs like an SPA.
  • Route rules pick a mode per path: prerender, swr, isr, ssr: false. You tried them in Server and Static Rendering.
  • npm run build makes a Node.js server in .output/. Run it with node .output/server/index.mjs.
  • npm run generate prerenders every page into static files, for any static host.
  • Nitro presets build for a host: Vercel, Netlify, Cloudflare and others, often found automatically.
You wantPlain Vue + ViteNuxt
RoutesWrite routes by handFiles in pages/
SSREntry files and a serverBuilt in
Data for SSRonServerPrefetch + state transferuseFetch / useAsyncData
Your own APIA separate serverserver/api/
Head tagsuseSSRContext by handuseSeoMeta / useHead

Simple lesson: pages in pages/, API in server/api/, data with useFetch, modes with route rules.

TL;DR

  • Nuxt = Vue + file routing + SSR + a Nitro server, wired together.
  • pages/ = one file per route. [id] is a param, [...slug] catches all.
  • Layouts = layouts/default.vue with <slot />. Pick one with definePageMeta.
  • useFetch / useAsyncData = SSR data with a key, sent in the payload, not fetched twice.
  • $fetch = a plain request for event handlers.
  • server/api/ = endpoints with defineEventHandler, readBody, createError.
  • Middleware = defineNuxtRouteMiddleware + navigateTo().
  • useState = SSR-safe shared state by key.
  • Deploy = build for a server, generate for static files, route rules for hybrid.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Writing a router by hand in NuxtNuxt needs routesAdd files to pages/Duplicate or broken routes
$fetch at the top of setupIt runs onceUse useFetch / useAsyncDataTwo requests and flicker
Same key for different dataKeys are just labelsOne unique key per dataComponents show the wrong data
Module-level ref for shared stateModules are per userUse useState or PiniaUsers see each other's data
Secrets in runtimeConfig.publicConfig is privateKeep secrets outside publicSecrets reach the browser
window in setupNuxt runs in the browser onlyUse onMounted or <ClientOnly>The server crashes
Nuxt for a login-only dashboardNuxt is always betterPlain Vue + Vite may be simplerExtra server cost for no SEO gain

Check Yourself

Exercises

What to Remember

  • Nuxt wires routing, SSR, data loading and a server around normal Vue.
  • Add pages in pages/, layouts in layouts/, endpoints in server/api/.
  • Use useFetch or useAsyncData with a unique key for page data, $fetch for actions.
  • Use useState or Pinia for shared state, never a module-level ref.
  • Pick a mode per route with route rules, then build or generate.

Next: Testing Vue Apps