A complete guide to full-stack development with Nuxt 3. Server routes, API layer, Nitro engine, auto-imports and edge deployment.
Introduction to Nuxt 3¶
Nuxt 3 is a full-stack framework built on Vue 3, powered by the Nitro engine. It provides everything needed to build modern web applications with server-side rendering, API routes, and edge deployment capabilities.
Key features of Nuxt 3: - Vue 3 & Composition API - Modern reactive framework - Nitro engine - Universal deployment and server engine - Auto-imports - Zero-config imports for components and composables - TypeScript - First-class TypeScript support - File-based routing - Automatic route generation - Server API - Built-in API layer
Project Structure¶
project/
├── components/ # Vue components (auto-imported)
├── pages/ # File-based routing
├── server/
│ └── api/ # API endpoints
├── composables/ # Vue composables (auto-imported)
├── middleware/ # Route middleware
├── plugins/ # Nuxt plugins
├── layouts/ # Layout components
└── nuxt.config.ts # Configuration
Pages and Routing¶
File-based routing with automatic code splitting:
<!-- pages/index.vue -->
<template>
<div>
<h1>{{ data.title }}</h1>
<NuxtLink to="/about">About</NuxtLink>
</div>
</template>
<script setup>
// Auto-imported composables
const { data } = await useFetch('/api/homepage')
// SEO with built-in composables
useSeoMeta({
title: 'My Nuxt App',
ogDescription: 'My amazing site.',
ogImage: '/image.png'
})
</script>
Dynamic routes with parameters:
<!-- pages/blog/[slug].vue -->
<template>
<article>
<h1>{{ post.title }}</h1>
<div v-html="post.content" />
</article>
</template>
<script setup>
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)
if (!post.value) {
throw createError({ statusCode: 404, statusMessage: 'Post not found' })
}
</script>
Server API Routes¶
Create API endpoints in the server/api directory:
// server/api/posts/index.get.ts
export default defineEventHandler(async (event) => {
// Query parameters
const query = getQuery(event)
const limit = query.limit || 10
// Mock data (replace with database query)
const posts = await $fetch('https://api.example.com/posts', {
params: { limit }
})
return {
posts,
meta: {
total: posts.length,
limit
}
}
})
POST endpoint with validation:
// server/api/posts/index.post.ts
import { z } from 'zod'
const createPostSchema = z.object({
title: z.string().min(1),
content: z.string().min(10),
authorId: z.number()
})
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validate input
const result = createPostSchema.safeParse(body)
if (!result.success) {
throw createError({
statusCode: 400,
statusMessage: 'Validation failed',
data: result.error.errors
})
}
// Create post (replace with database logic)
const post = await createPost(result.data)
return { post }
})
Composables and State Management¶
Create reusable composables:
// composables/useAuth.ts
export const useAuth = () => {
const user = useState('auth.user', () => null)
const isLoggedIn = computed(() => !!user.value)
const login = async (credentials: LoginCredentials) => {
const { data } = await $fetch('/api/auth/login', {
method: 'POST',
body: credentials
})
user.value = data.user
await navigateTo('/dashboard')
}
const logout = async () => {
await $fetch('/api/auth/logout', { method: 'POST' })
user.value = null
await navigateTo('/login')
}
return {
user,
isLoggedIn,
login,
logout
}
}
Use in components:
<template>
<div>
<div v-if="isLoggedIn">
Welcome, {{ user.name }}!
<button @click="logout">Logout</button>
</div>
<div v-else>
<LoginForm @submit="login" />
</div>
</div>
</template>
<script setup>
const { user, isLoggedIn, login, logout } = useAuth()
</script>
Middleware and Layouts¶
Route middleware for authentication:
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isLoggedIn } = useAuth()
if (!isLoggedIn.value) {
return navigateTo('/login')
}
})
Layout with navigation:
<!-- layouts/default.vue -->
<template>
<div class="app">
<header class="header">
<nav>
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/about">About</NuxtLink>
<NuxtLink to="/blog">Blog</NuxtLink>
</nav>
</header>
<main>
<slot />
</main>
</div>
</template>
<style scoped>
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: #f8f9fa;
padding: 1rem;
}
</style>
Configuration and Deployment¶
Basic Nuxt configuration:
// nuxt.config.ts
export default defineNuxtConfig({
// Development tools
devtools: { enabled: true },
// CSS framework
css: ['~/assets/css/main.css'],
// Build configuration
nitro: {
preset: 'vercel-edge' // or 'netlify', 'cloudflare-pages'
},
// Runtime config
runtimeConfig: {
// Private keys (server-side only)
apiSecret: process.env.API_SECRET,
// Public keys (exposed to client)
public: {
apiBase: process.env.API_BASE || '/api'
}
},
// Modules
modules: [
'@pinia/nuxt',
'@nuxtjs/tailwindcss'
]
})
Deploy to Vercel:
# Build for production
npm run build
# Deploy to Vercel
vercel --prod
Best Practices¶
- Use auto-imports - Leverage Nuxt’s zero-config imports
- Server-side state - Use
useStatefor reactive server state - SEO optimization - Use
useSeoMetaanduseHead - Error handling - Implement proper error pages and handling
- TypeScript - Enable TypeScript for better DX
- Composables - Create reusable logic with composables
Nuxt 3 provides a powerful full-stack development experience with Vue 3, combining client-side reactivity with server-side capabilities in a unified framework.