Nuxt.js is the Vue.js meta-framework that provides server-side rendering (SSR), static site generation (SSG), and a powerful file-based routing system. It simplifies building performant, SEO-friendly Vue applications. This guide covers Nuxt 3 development from a senior developer's perspective.
Why Nuxt.js
Nuxt provides significant advantages:
- SEO-Friendly: Server-side rendering for search engines
- Performance: Automatic code splitting and optimization
- File-Based Routing: No router configuration needed
- Auto-Imports: Components and composables available everywhere
- Full-Stack: API routes and server middleware built in
Getting Started
Create Project
npx nuxi@latest init my-app
cd my-app
npm install
npm run dev
Project Structure
my-app/
├── .nuxt/ # Generated files (.gitignore)
├── assets/ # Uncompiled assets (Sass, images)
├── components/ # Vue components (auto-imported)
├── composables/ # Vue composables (auto-imported)
├── layouts/ # Page layouts
├── middleware/ # Route middleware
├── pages/ # File-based routes
├── plugins/ # Vue plugins
├── public/ # Static files (served as is)
├── server/
│ ├── api/ # API routes
│ └── middleware/ # Server middleware
├── nuxt.config.ts # Nuxt configuration
└── app.vue # Root component
File-Based Routing
Basic Routes
Create files in the pages/ directory:
pages/
├── index.vue # /
├── about.vue # /about
├── contact.vue # /contact
└── users/
├── index.vue # /users
└── [id].vue # /users/:id (dynamic)
Dynamic Routes
pages/users/[id].vue:
<script setup>
const route = useRoute()
const userId = route.params.id
// Fetch user data
const { data: user } = await useFetch(`/api/users/${userId}`)
</script>
<template>
<div>
<h1>{{ user?.name }}</h1>
<p>{{ user?.email }}</p>
</div>
</template>
Nested Routes
pages/
└── users/
├── index.vue # /users
├── [id].vue # /users/:id
└── [id]/
├── posts.vue # /users/:id/posts
└── settings.vue # /users/:id/settings
Data Fetching
useFetch Composable
<script setup>
// Basic fetch
const { data, pending, error, refresh } = await useFetch('/api/posts')
// With options
const { data: posts } = await useFetch('/api/posts', {
query: { limit: 10, page: 1 },
headers: { 'Authorization': 'Bearer token' },
transform: (response) => response.data,
})
// Lazy loading (doesn't block navigation)
const { data: comments, pending } = useLazyFetch('/api/comments')
// Watch for changes and refetch
const page = ref(1)
const { data } = await useFetch('/api/posts', {
query: { page },
watch: [page],
})
</script>
<template>
<div>
<div v-if="pending">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="post in posts" :key="post.id">
{{ post.title }}
</li>
</ul>
</div>
</template>
useAsyncData for Custom Logic
<script setup>
const { data: user } = await useAsyncData('user', async () => {
// Custom async logic
const profile = await $fetch('/api/profile')
const settings = await $fetch('/api/settings')
return {
...profile,
settings,
}
})
</script>
State Management with Pinia
Install and Configure
npm install @pinia/nuxt
Add to nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
})
Create Store
stores/counter.ts:
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0)
// Getters
const doubleCount = computed(() => count.value * 2)
// Actions
function increment() {
count.value++
}
async function fetchCount() {
const { data } = await useFetch('/api/count')
count.value = data.value
}
return { count, doubleCount, increment, fetchCount }
})
Use Store in Component
<script setup>
const counter = useCounterStore()
</script>
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+1</button>
</div>
</template>
Server Routes (API)
Create API Endpoint
server/api/users/index.get.ts:
export default defineEventHandler(async (event) => {
// Access query parameters
const query = getQuery(event)
const { page = 1, limit = 10 } = query
// Fetch from a database or external API
const users = await fetchUsers({ page, limit })
return {
data: users,
meta: { page, limit },
}
})
server/api/users/[id].get.ts:
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const user = await fetchUser(id)
if (!user) {
throw createError({
statusCode: 404,
message: 'User not found',
})
}
return user
})
POST Request Handler
server/api/users/index.post.ts:
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validate
if (!body.email || !body.name) {
throw createError({
statusCode: 400,
message: 'Name and email required',
})
}
// Create user
const user = await createUser(body)
setResponseStatus(event, 201)
return user
})
Layouts
Default Layout
layouts/default.vue:
<template>
<div class="layout">
<header>
<nav>
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/about">About</NuxtLink>
</nav>
</header>
<main>
<slot />
</main>
<footer>
© {{ new Date().getFullYear() }}
</footer>
</div>
</template>
<style scoped>
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
</style>
Custom Layout
layouts/admin.vue:
<template>
<div class="admin-layout">
<aside class="sidebar">
<!-- Admin navigation -->
</aside>
<main class="content">
<slot />
</main>
</div>
</template>
Use in a page:
<script setup>
definePageMeta({
layout: 'admin',
})
</script>
Middleware
Route Middleware
middleware/auth.ts:
export default defineNuxtRouteMiddleware((to, from) => {
const user = useAuthUser()
if (!user.value) {
return navigateTo('/login')
}
})
Apply to a page:
<script setup>
definePageMeta({
middleware: ['auth'],
})
</script>
Global Middleware
middleware/analytics.global.ts:
export default defineNuxtRouteMiddleware((to, from) => {
// Track page view
trackPageView(to.path)
})
SEO and Meta
Page Meta
<script setup>
useHead({
title: 'My Page Title',
meta: [
{ name: 'description', content: 'Page description for SEO' },
{ property: 'og:title', content: 'My Page Title' },
{ property: 'og:image', content: '/og-image.png' },
],
link: [
{ rel: 'canonical', href: 'https://example.com/page' },
],
})
</script>
Dynamic Meta
<script setup>
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)
useHead({
title: () => post.value?.title || 'Loading...',
meta: [
{ name: 'description', content: () => post.value?.excerpt },
],
})
</script>
Environment Configuration
Runtime Config
nuxt.config.ts:
export default defineNuxtConfig({
runtimeConfig: {
// Private keys (server only)
apiSecret: process.env.API_SECRET,
// Public keys (exposed to the client)
public: {
apiBase: process.env.API_BASE_URL || '/api',
},
},
})
Access in code:
<script setup>
const config = useRuntimeConfig()
// Server-side only
// config.apiSecret
// Client and server
const apiBase = config.public.apiBase
</script>
Deployment
Static Generation
npm run generate
Generates static HTML in .output/public/.
SSR Deployment
npm run build
Deploy the .output/ directory. For Node.js:
node .output/server/index.mjs
Serverless (AWS Lambda)
nuxt.config.ts:
export default defineNuxtConfig({
nitro: {
preset: 'aws-lambda',
},
})
Build and deploy:
npm run build
# Deploy .output/ to AWS Lambda
Key Takeaways
- File-based routing: Convention over configuration
- Auto-imports: Less boilerplate, more productivity
- useFetch for data: Built-in caching and SSR support
- Server routes: Full-stack in one project
- Pinia for state: Simple, type-safe state management
- Flexible deployment: SSR, SSG, or hybrid
Nuxt 3 dramatically simplifies Vue.js development while providing enterprise-grade features for SEO, performance, and full-stack capabilities.