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>© 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
Related reading
- 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.