15.1 Introduction to Nuxt — Vue-Based Full-Stack Framework
What is Nuxt?
Nuxt is a full-stack web framework built on top of Vue.js. While Vue alone only supports Client-Side Rendering (CSR), Nuxt enables Server-Side Rendering (SSR), Static Site Generation (SSG), and even a full-stack API server — all within a single project.
Nuxt 3, officially released in late 2022, adopts Vue 3, Vite, and TypeScript by default, delivering a modern web development experience.
Problems Nuxt Solves
| Problem | Vue Only | With Nuxt |
|---|---|---|
| SEO optimization | Difficult (CSR) | Built-in (SSR/SSG) |
| Routing setup | Manual configuration | File-based auto-generation |
| API server | Separate backend required | Built-in API routes |
| Code splitting | Manual setup | Automatic |
| SEO meta tags | Manual management | useHead composable |
| State management | Install Pinia separately | Built-in useState |
Understanding Rendering Modes
One of the most powerful features of Nuxt is the ability to flexibly choose your rendering mode.
1. SSR (Server-Side Rendering)
The server completes the HTML and sends it to the browser. Favorable for SEO and fast initial loading.
Request → Vue rendered on server → Complete HTML sent → Hydration in browser
2. SSG (Static Site Generation)
All pages are generated as HTML at build time. Ideal for blogs and documentation sites.
Build → All pages generated as HTML → CDN deployment → Static files returned on request
3. CSR (Client-Side Rendering)
The traditional Vue SPA approach. JavaScript renders content in the browser.
Request → Empty HTML + JS bundle sent → Rendered in browser
4. ISR (Incremental Static Regeneration)
Combines the benefits of SSG and SSR. Static pages are periodically regenerated.
First request → Generated and cached on server → Subsequent requests serve cached page → Periodic refresh
File-Based Routing
One of Nuxt's core features is file system-based routing. Creating a file in the pages/ directory automatically generates a route.
Basic Routing
pages/
├── index.vue → /
├── about.vue → /about
├── contact.vue → /contact
└── blog/
├── index.vue → /blog
└── [slug].vue → /blog/:slug (dynamic route)
Dynamic Route Example
<!-- pages/blog/[slug].vue -->
<template>
<div>
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
</div>
</template>
<script setup lang="ts">
// Access current route params with useRoute()
const route = useRoute()
const slug = route.params.slug // /blog/hello-world → slug = 'hello-world'
const { data: post } = await useFetch(`/api/posts/${slug}`)
</script>
Nested Routes
pages/
└── user/
├── [id]/
│ ├── index.vue → /user/:id
│ └── settings.vue → /user/:id/settings
└── index.vue → /user
Catch-All Routes
pages/
└── [...slug].vue → matches /a, /a/b, /a/b/c
<!-- pages/[...slug].vue -->
<script setup lang="ts">
const route = useRoute()
// /docs/guide/intro → slug = ['docs', 'guide', 'intro']
console.log(route.params.slug)
</script>
The Auto-Import System
One of Nuxt 3's most innovative features is Auto-import. You can use Vue composables, utilities, and components directly without writing import statements.
What Gets Auto-Imported
Vue Composables
<script setup lang="ts">
// No need for: import { ref, computed, watch } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
watch(count, (newVal) => {
console.log('count changed:', newVal)
})
</script>
Nuxt Composables
<script setup lang="ts">
// Use directly without import
const route = useRoute()
const router = useRouter()
const config = useRuntimeConfig()
const { data } = await useFetch('/api/users')
</script>
The composables/ Directory
// composables/useCounter.ts
export const useCounter = () => {
const count = ref(0)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
<!-- Use anywhere without import -->
<script setup lang="ts">
const { count, increment } = useCounter()
</script>
The utils/ Directory
// utils/format.ts
export const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US').format(date)
}
export const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount)
}
<script setup lang="ts">
// Use directly without import
const price = formatCurrency(15000) // '$15,000.00'
</script>
Component Auto-Import
components/
├── Button.vue → <Button />
├── ui/
│ ├── Card.vue → <UiCard />
│ └── Modal.vue → <UiModal />
└── base/
└── Input.vue → <BaseInput />
Practical Example: A Simple Blog App
Here is a simple blog app example using Nuxt's core features.
Project Structure
my-blog/
├── pages/
│ ├── index.vue # Blog listing
│ └── posts/
│ └── [id].vue # Post detail
├── components/
│ └── PostCard.vue # Post card component
├── composables/
│ └── usePosts.ts # Post-related logic
├── server/
│ └── api/
│ ├── posts/
│ │ ├── index.get.ts # GET /api/posts
│ │ └── [id].get.ts # GET /api/posts/:id
└── nuxt.config.ts
Writing Server API Routes
// server/api/posts/index.get.ts
export default defineEventHandler(async (event) => {
// In practice, fetch from a database; hardcoded here for the example
return [
{
id: 1,
title: 'Getting Started with Nuxt 3',
summary: 'Explore the core features of Nuxt 3.',
date: '2024-01-15',
author: 'John Smith'
},
{
id: 2,
title: 'Vue 3 Composition API',
summary: 'Learn the benefits and usage of Composition API.',
date: '2024-01-20',
author: 'Jane Doe'
},
{
id: 3,
title: 'TypeScript with Nuxt',
summary: 'How to use TypeScript effectively in Nuxt.',
date: '2024-01-25',
author: 'Bob Johnson'
}
]
})
// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const posts: Record<string, object> = {
'1': {
id: 1,
title: 'Getting Started with Nuxt 3',
content: `
# Getting Started with Nuxt 3
Nuxt 3 is a full-stack framework built on Vue.js...
## Installation
\`\`\`bash
npx nuxi@latest init my-app
\`\`\`
`,
date: '2024-01-15',
author: 'John Smith'
}
}
const post = posts[id as string]
if (!post) {
throw createError({ statusCode: 404, message: 'Post not found.' })
}
return post
})
Writing a Composable
// composables/usePosts.ts
interface Post {
id: number
title: string
summary: string
date: string
author: string
}
export const usePosts = () => {
const posts = useState<Post[]>('posts', () => [])
const fetchPosts = async () => {
const { data } = await useFetch<Post[]>('/api/posts')
if (data.value) {
posts.value = data.value
}
}
return { posts, fetchPosts }
}
Blog Listing Page
<!-- pages/index.vue -->
<template>
<div class="container">
<h1>Blog</h1>
<div v-if="pending" class="loading">
Loading...
</div>
<div v-else-if="error" class="error">
An error occurred: {{ error.message }}
</div>
<div v-else class="post-list">
<PostCard
v-for="post in posts"
:key="post.id"
:post="post"
/>
</div>
</div>
</template>
<script setup lang="ts">
interface Post {
id: number
title: string
summary: string
date: string
author: string
}
// useFetch works with SSR, which is great for SEO
const { data: posts, pending, error } = await useFetch<Post[]>('/api/posts')
// SEO configuration
useHead({
title: 'Blog - Latest Posts',
meta: [
{ name: 'description', content: 'Discover the latest posts about Vue and Nuxt.' }
]
})
</script>
<style scoped>
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.post-list {
display: grid;
gap: 1.5rem;
}
.loading, .error {
text-align: center;
padding: 2rem;
}
</style>
Post Card Component
<!-- components/PostCard.vue -->
<template>
<article class="post-card">
<NuxtLink :to="`/posts/${post.id}`">
<h2>{{ post.title }}</h2>
</NuxtLink>
<p>{{ post.summary }}</p>
<footer>
<span>{{ post.author }}</span>
<time>{{ formatDate(post.date) }}</time>
</footer>
</article>
</template>
<script setup lang="ts">
interface Post {
id: number
title: string
summary: string
date: string
author: string
}
defineProps<{
post: Post
}>()
// formatDate is auto-imported from utils/format.ts
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
</script>
Post Detail Page
<!-- pages/posts/[id].vue -->
<template>
<div class="post-detail">
<NuxtLink to="/">← Back to list</NuxtLink>
<article v-if="post">
<h1>{{ post.title }}</h1>
<div class="meta">
<span>{{ post.author }}</span>
<time>{{ post.date }}</time>
</div>
<!-- In practice, use a markdown renderer -->
<div class="content" v-html="post.content" />
</article>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
interface Post {
id: number
title: string
content: string
date: string
author: string
}
const { data: post, error } = await useFetch<Post>(`/api/posts/${route.params.id}`)
if (error.value) {
throw createError({ statusCode: 404, message: 'Post not found.' })
}
// Dynamic SEO meta tags
useHead({
title: () => post.value ? `${post.value.title} - Blog` : 'Blog',
meta: [
{
name: 'description',
content: () => post.value?.content?.slice(0, 160) || ''
}
]
})
</script>
Expert Tips
1. Understanding the Nitro Server Engine
Nuxt 3 uses a server engine called Nitro. Nitro supports a wide variety of deployment targets.
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages', // For Cloudflare Pages
// preset: 'vercel', // For Vercel
// preset: 'node-server', // For a Node.js server
// preset: 'static', // For static deployment
}
})
2. Adding Global Functionality with Plugins
// plugins/errorHandler.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.config.errorHandler = (error, instance, info) => {
console.error('Vue error:', error, info)
// Send to an error reporting service
}
nuxtApp.hook('vue:error', (error) => {
console.error('Nuxt error hook:', error)
})
})
3. Handling Authentication with Middleware
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useSupabaseUser() // or your own auth state
// Redirect unauthenticated users away from protected pages
if (!user.value && to.path !== '/login') {
return navigateTo('/login')
}
})
<!-- pages/dashboard.vue -->
<script setup lang="ts">
// Apply middleware only to this page
definePageMeta({
middleware: 'auth'
})
</script>
4. Leveraging the Layout System
<!-- layouts/default.vue -->
<template>
<div>
<AppHeader />
<main>
<slot /> <!-- Page content goes here -->
</main>
<AppFooter />
</div>
</template>
<!-- layouts/admin.vue -->
<template>
<div class="admin-layout">
<AdminSidebar />
<main>
<slot />
</main>
</div>
</template>
<!-- pages/admin/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'admin' // Use the admin layout
})
</script>
5. Type-Safe API Routes
// server/api/users/[id].get.ts
interface User {
id: number
name: string
email: string
}
export default defineEventHandler(async (event): Promise<User> => {
const id = parseInt(getRouterParam(event, 'id') as string)
// In practice, query the database
return {
id,
name: 'John Smith',
email: 'john@example.com'
}
})
<!-- pages/users/[id].vue -->
<script setup lang="ts">
const route = useRoute()
// Types are inferred automatically
const { data: user } = await useFetch(`/api/users/${route.params.id}`)
// user.value is typed as { id: number, name: string, email: string }
</script>
6. Separating Server and Client Code
// server/utils/db.ts (runs only on server)
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY! // Secret key used only on the server
)
// composables/useAuth.ts (runs on client)
export const useAuth = () => {
const user = useState('user', () => null)
const login = async (email: string, password: string) => {
// Only use the public key
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
user.value = response.user
}
return { user, login }
}
Summary
Nuxt 3 is not simply Vue with SSR added on top. Through file-based routing, Auto-import, built-in server APIs, and flexible rendering modes, it is a comprehensive solution for rapidly building full-stack web applications.
The Auto-import system in particular greatly improves the developer experience. Without needing to write import statements, your code becomes cleaner and your productivity increases.
In the next chapter, we will set up an actual Nuxt project and explore its directory structure in detail.