The Complete Nuxt 3 Guide | Vue, SSR, Composables, Nitro, Server Routes

The Complete Nuxt 3 Guide | Vue, SSR, Composables, Nitro, Server Routes

What this post covers

This is a complete guide to building full-stack Vue applications with Nuxt 3. It covers Auto-imports, Composables, the Nitro engine, Server Routes, and deployment—with practical examples you can apply in real projects.

From the field: Migrating a Vue SPA to Nuxt 3, we significantly improved SEO and roughly tripled initial load performance—here is what we learned.

Introduction: “Vue SPAs are weak on SEO”

Real-world scenarios

Scenario 1: Search engines do not index the site well

SPAs are weak for SEO. Nuxt SSR addresses this. Scenario 2: Initial load feels slow

The browser must download all JavaScript first. Nuxt can render on the server. Scenario 3: Imports are tedious

You repeat imports everywhere. Nuxt auto-imports components and composables.


1. What is Nuxt 3?

Core characteristics

Nuxt 3 is a full-stack framework built on Vue 3.

Key benefits:

  • Auto-imports: automatic imports
  • Nitro: a fast server engine
  • SSR/SSG: choose your rendering mode
  • Server Routes: APIs built into the app
  • TypeScript: first-class support

2. Creating a project

Run the following in your terminal to scaffold and start the dev server.

npx nuxi@latest init my-app
cd my-app
npm install
npm run dev

3. File-based routing

Pages

pages/index.vue defines the home route:

<!-- pages/index.vue -->
<template>
  <div>
    <h1>Home</h1>
    <NuxtLink to="/about">About</NuxtLink>
  </div>
</template>

pages/about.vue defines the /about route:

<!-- pages/about.vue -->
<!-- Example -->
<template>
  <div>
    <h1>About</h1>
  </div>
</template>

Dynamic routes

Dynamic segments use bracket syntax. useRoute reads params, and useFetch loads data for the current slug:

<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute();
const slug = route.params.slug;
const { data: post } = await useFetch(`/api/posts/${slug}`);
</script>
<template>
  <article>
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
  </article>
</template>

4. Composables

useFetch

useFetch integrates with Nuxt’s data layer: it exposes data, loading state, errors, and refresh.

<script setup lang="ts">
const { data, pending, error, refresh } = await useFetch('/api/users');
</script>
<template>
  <div>
    <div v-if="pending">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <ul v-else>
      <li v-for="user in data" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
    <button @click="refresh">Refresh</button>
  </div>
</template>

useAsyncData

Use useAsyncData when you need a stable key and custom fetch logic (for deduplication and caching across the app):

<script setup lang="ts">
const { data: posts } = await useAsyncData('posts', () => 
  $fetch('/api/posts')
);
</script>

Custom composable

// composables/useAuth.ts
export const useAuth = () => {
  const user = useState('user', () => null);
  const login = async (email: string, password: string) => {
    const response = await $fetch('/api/login', {
      method: 'POST',
      body: { email, password },
    });
    user.value = response.user;
  };
  const logout = async () => {
    await $fetch('/api/logout', { method: 'POST' });
    user.value = null;
  };
  return { user, login, logout };
};

5. Server routes

API endpoints

HTTP method and path map to files under server/api. A GET handler for /api/users:

// server/api/users.get.ts
export default defineEventHandler(async (event) => {
  const users = await prisma.user.findMany();
  return users;
});

Dynamic GET /api/users/:id with a 404 when the record is missing:

// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = parseInt(event.context.params.id);
  const user = await prisma.user.findUnique({ where: { id } });
  if (!user) {
    throw createError({
      statusCode: 404,
      statusMessage: 'User not found',
    });
  }
  return user;
});

POST /api/users to create a user:

// server/api/users.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  const user = await prisma.user.create({
    data: {
      name: body.name,
      email: body.email,
    },
  });
  return user;
});

6. Middleware

Auth middleware

Route middleware runs before navigation. This example redirects unauthenticated users to /login:

// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { user } = useAuth();
  if (!user.value && to.path !== '/login') {
    return navigateTo('/login');
  }
});

Applying middleware

Register middleware per page with definePageMeta:

<!-- pages/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: 'auth',
});
</script>
<template>
  <div>
    <h1>Dashboard</h1>
  </div>
</template>

7. Layouts

Layouts wrap pages with shared chrome (header, footer, nav). The default slot renders the page content.

<!-- layouts/default.vue -->
<template>
  <div>
    <header>
      <nav>
        <NuxtLink to="/">Home</NuxtLink>
        <NuxtLink to="/about">About</NuxtLink>
      </nav>
    </header>
    <main>
      <slot />
    </main>
    <footer>
      <p>&copy; 2026 My App</p>
    </footer>
  </div>
</template>

Select a layout from a page:

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

8. Deployment

Static (SSG)

npm run generate

Server (SSR)

npm run build
node .output/server/index.mjs

Docker

Multi-stage build: compile the app in a builder image, then copy only .output into a slim runtime image.

FROM node:20-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.output .output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

Summary and checklist

Key takeaways

  • Nuxt 3: full-stack framework on Vue 3
  • Auto-imports: automatic imports
  • Nitro: fast server engine
  • useFetch: data fetching with SSR awareness
  • Server Routes: APIs colocated with the app
  • SSR/SSG: pick the rendering mode that fits your product

Implementation checklist

  • Create a Nuxt 3 project
  • Implement page routing
  • Fetch data with useFetch
  • Add Server Routes
  • Implement middleware
  • Configure layouts
  • Deploy

  • Vue 3 Composition API guide
  • Next.js App Router guide
  • SvelteKit complete guide

Keywords covered in this post

Nuxt, Vue, Full Stack, SSR, Nitro, Web Framework, TypeScript

Frequently asked questions (FAQ)

Q. Nuxt 2 vs Nuxt 3—which should I use?

A. Nuxt 3 is much faster and more modern. Prefer Nuxt 3 for new projects.

Q. Next.js vs Nuxt—which is better?

A. Choose Nuxt if you prefer Vue, and Next.js if you prefer React. Capabilities are broadly similar.

Q. Does Nuxt use Vite?

A. Yes—Nuxt 3 uses Vite by default.

Q. Is Nuxt 3 production-ready?

A. Yes. Nuxt 3 is stable and widely used in production.