# Nuxt: Full-Stack Vue

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Rendering Modes
Source: https://www.devtutorials.ai/learn/vue/rendering-modes/nuxt-full-stack-vue

> 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.

    <h3>Welcome to Meera's Kitchen</h3>
</template>
`,
    "MenuPage.vue": `<template>
    <h3>Menu</h3>
    <p>Dosa · Idli · Vada</p>
</template>
`,
    "DishPage.vue": `<script setup lang="ts">

const route = useRoute();
</script>

<template>
    <h3>Dish {{ route.params.id }}</h3>
</template>
`,
    "NotFound.vue": `<template>
    <h3>Page not found</h3>
</template>
`,
    "main.ts": `import { createApp } from "vue";

const routes = fromFiles({
    "pages/index.vue": HomePage,
    "pages/menu/index.vue": MenuPage,
    "pages/dish/[id].vue": DishPage,
    "pages/[...slug].vue": NotFound,
});

createApp(App).use(createRouter({ history: createWebHistory(), routes })).mount("#app");
`,
    "App.vue": `<template>
    <nav>
    </nav>
</template>
`,
  }}
/>

- **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

```bash
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/
```

<div className="tscroll">

| 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 |

</div>

- **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.

```vue
<!-- 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>
```

    <div style="border: 2px solid seagreen; padding: 8px">
      <b>Meera's Kitchen</b>
      <slot />
      <small>Call 98765 43210</small>
    </div>
</template>
`,
    "AdminLayout.vue": `<template>
    <div style="border: 2px solid crimson; padding: 8px">
      <b>Admin area</b>
      <slot />
    </div>
</template>
`,
    "main.ts": `import { createApp, h } from "vue";

const routes = [
    { path: "/", component: { render: () => h("p", "Today's specials") } },
    { path: "/admin", component: { render: () => h("p", "12 orders waiting") }, meta: { layout: "admin" } },
];

createApp(App).use(createRouter({ history: createWebHistory(), routes })).mount("#app");
`,
    "App.vue": `<script setup lang="ts">

const route = useRoute();
const layouts = { default: DefaultLayout, admin: AdminLayout };
const layout = computed(() => (route.meta.layout === "admin" ? layouts.admin : layouts.default));
</script>

<template>
    <RouterLink to="/">Home</RouterLink> | <RouterLink to="/admin">Admin</RouterLink>
    <component :is="layout">
    </component>
</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.

```vue
<script setup lang="ts">
const { data: dishes, status, error, refresh } = await useFetch("/api/dishes");
const { data: today } = await useAsyncData("today", () => $fetch("/api/specials"));
</script>
```

const { data, status } = useAsyncData("dishes", () => fetch("https://api.meeraskitchen.dev/dishes").then((r) => r.json()));
</script>

<template>
    <p>Header: {{ status === "success" ? \`\${data.length} dishes\` : "loading…" }}</p>
</template>
`,
    "DishList.vue": `<script setup lang="ts">

const { data, status, refresh } = useAsyncData("dishes", () => fetch("https://api.meeraskitchen.dev/dishes").then((r) => r.json()));
</script>

<template>
    <p v-if="status === 'pending'">Loading…</p>
    <ul v-else><li v-for="d in data" :key="d.id">{{ d.name }}</li></ul>
    <button @click="refresh">Refresh</button>
</template>
`,
    "App.vue": `<script setup lang="ts">
</script>

<template>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **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.

```ts
// 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;
});
```

const result = ref<string>("");

async function run(url: string, method = "GET", body?: unknown): Promise<void> {
    try {
      result.value = JSON.stringify(await $fetch(url, { method, body }));
    } catch (e) {
      result.value = \`Error \${(e as { statusCode: number }).statusCode}: \${(e as { statusMessage: string }).statusMessage}\`;
    }
}
</script>

<template>
    <button @click="run('/api/dishes')">GET /api/dishes</button>
    <button @click="run('/api/dishes/2')">GET /api/dishes/2</button>
    <button @click="run('/api/dishes/9')">GET /api/dishes/9</button>
    <button @click="run('/api/orders', 'POST', { dish: 'Dosa', table: 4 })">POST /api/orders</button>
    <pre>{{ result }}</pre>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

## 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.

```ts
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
  const user = useState<string | null>("user", () => null);
  if (!user.value) return navigateTo(`/login?redirect=${to.fullPath}`);
});
```

const user = useState<string | null>("user", () => null);
const route = useRoute();
const router = useRouter();

function logIn(): void {
    user.value = "meera";
    router.push(String(route.query.redirect ?? "/"));
}
</script>

<template>
    <button @click="logIn">Log in as Meera</button>
</template>
`,
    "App.vue": `<script setup lang="ts">

const user = useState<string | null>("user", () => null);
</script>

<template>
    <p>User: {{ user ?? "guest" }}</p>
    <RouterLink to="/">Home</RouterLink> | <RouterLink to="/admin">Admin</RouterLink>
</template>
`,
  }}
/>

## 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`.

```ts
// 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.

<div className="tscroll">

| 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` |

</div>

**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

<div className="tscroll">

| 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 |

</div>

## 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**
