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.tsormain.ts. You only add files topages/.
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
- When to choose Nuxt, and how to create a project.
- The Nuxt 4 folders and auto-imports.
- File-based routing, dynamic pages and
<NuxtLink>. - Layouts.
useFetchanduseAsyncData: keys, status and the payload.- Server API routes with Nitro.
- Route middleware and
useState. - SEO, runtime config and modules.
- 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 file | Holds |
|---|---|
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.vue | The root component |
server/api/ | Your API endpoints (Nitro) |
public/ | Files served as they are |
nuxt.config.ts | Settings, modules and route rules |
- Auto-imports:
ref,computed,useRoute, your components and composables are available withoutimportlines. - 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 withuseRoute().params.id.pages/[...slug].vuecatches 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.vueis 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>anddefinePageMeta({ 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),errorandrefresh(). 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,getQueryandreadBody. Send errors withcreateError. - 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 indefinePageMeta({ 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/nuxtmodule 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
publicreach 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 buildmakes a Node.js server in.output/. Run it withnode .output/server/index.mjs.npm run generateprerenders every page into static files, for any static host.- Nitro presets build for a host: Vercel, Netlify, Cloudflare and others, often found automatically.
| You want | Plain Vue + Vite | Nuxt |
|---|---|---|
| Routes | Write routes by hand | Files in pages/ |
| SSR | Entry files and a server | Built in |
| Data for SSR | onServerPrefetch + state transfer | useFetch / useAsyncData |
| Your own API | A separate server | server/api/ |
| Head tags | useSSRContext by hand | useSeoMeta / 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.vuewith<slot />. Pick one withdefinePageMeta. useFetch/useAsyncData= SSR data with a key, sent in the payload, not fetched twice.$fetch= a plain request for event handlers.server/api/= endpoints withdefineEventHandler,readBody,createError.- Middleware =
defineNuxtRouteMiddleware+navigateTo(). useState= SSR-safe shared state by key.- Deploy =
buildfor a server,generatefor static files, route rules for hybrid.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Writing a router by hand in Nuxt | Nuxt needs routes | Add files to pages/ | Duplicate or broken routes |
$fetch at the top of setup | It runs once | Use useFetch / useAsyncData | Two requests and flicker |
| Same key for different data | Keys are just labels | One unique key per data | Components show the wrong data |
Module-level ref for shared state | Modules are per user | Use useState or Pinia | Users see each other's data |
Secrets in runtimeConfig.public | Config is private | Keep secrets outside public | Secrets reach the browser |
window in setup | Nuxt runs in the browser only | Use onMounted or <ClientOnly> | The server crashes |
| Nuxt for a login-only dashboard | Nuxt is always better | Plain Vue + Vite may be simpler | Extra 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 inlayouts/, endpoints inserver/api/. - Use
useFetchoruseAsyncDatawith a unique key for page data,$fetchfor actions. - Use
useStateor Pinia for shared state, never a module-level ref. - Pick a mode per route with route rules, then
buildorgenerate.
Next: Testing Vue Apps