Nuxt Features Guide
Powerful features that make Nuxt the Intuitive Vue Framework
File-based Routing
Routes are automatically created from the pages/ directory structure.
Basic Routes
pages/
├── index.vue → /
├── about.vue → /about
├── blog/
│ ├── index.vue → /blog
│ └── [id].vue → /blog/:id
└── user/
└── [username]/
└── profile.vue → /user/:username/profile
Dynamic Routes
<!-- pages/blog/[id].vue -->
<template>
<div>
<h1>Blog Post: {{ route.params.id }}</h1>
</div>
</template>
<script setup>
const route = useRoute()
// Access params: route.params.id
</script>
Catch-all Routes
<!-- pages/docs/[...slug].vue -->
<template>
<div>
<h1>Docs: {{ route.params.slug.join('/') }}</h1>
</div>
</template>
<script setup>
const route = useRoute()
// /docs/getting-started → ['getting-started']
// /docs/guide/installation → ['guide', 'installation']
</script>
Layouts
<!-- layouts/default.vue -->
<template>
<div>
<AppHeader />
<slot /> <!-- Page content -->
<AppFooter />
</div>
</template>
<!-- pages/index.vue -->
<script setup>
definePageMeta({
layout: 'default' // or 'dashboard', 'auth', etc.
})
</script>
Data Fetching
Nuxt provides powerful composables for fetching data on server and client.
useFetch - Recommended
<script setup>
// Fetches on server and client
const { data, pending, error, refresh } = await useFetch('/api/users')
</script>
<template>
<div>
<p v-if="pending">Loading...</p>
<p v-else-if="error">Error: {{ error.message }}</p>
<ul v-else>
<li v-for="user in data" :key="user.id">
{{ user.name }}
</li>
</ul>
<button @click="refresh">Refresh</button>
</div>
</template>
useAsyncData - Advanced
<script setup>
const route = useRoute()
const { data: post } = await useAsyncData(
'post-' + route.params.id, // Unique key
() => $fetch(`/api/posts/${route.params.id}`)
)
</script>
useLazyFetch - Client-only
<script setup>
// Non-blocking, fetches on client
const { pending, data } = useLazyFetch('/api/posts')
</script>
<template>
<div>
<!-- Component renders immediately -->
<p v-if="pending">Loading posts...</p>
<PostList v-else :posts="data" />
</div>
</template>
Fetch Options
const { data } = await useFetch('/api/users', {
method: 'POST',
body: { name: 'John' },
headers: { 'Authorization': 'Bearer token' },
// Cache options
lazy: false,
server: true,
immediate: true,
// Transformations
transform: (data) => data.users,
default: () => [],
// Watch dependencies
watch: [someRef],
// Error handling
onRequest({ request, options }) {},
onResponse({ response }) {},
onResponseError({ response }) {}
})
Performance: useFetch automatically deduplicates requests and caches responses!
Composables
Reusable stateful logic with auto-imported composables.
Built-in Composables
| Composable | Purpose |
|---|---|
useRoute() |
Access current route information |
useRouter() |
Navigate programmatically |
useFetch() |
Fetch data from API |
useState() |
SSR-friendly reactive state |
useCookie() |
Read/write cookies |
useHead() |
Manage meta tags |
useRuntimeConfig() |
Access runtime configuration |
Custom Composable
// composables/useAuth.ts
export const useAuth = () => {
const user = useState('user', () => null)
const isLoggedIn = computed(() => !!user.value)
const login = async (credentials) => {
const data = await $fetch('/api/login', {
method: 'POST',
body: credentials
})
user.value = data.user
}
const logout = () => {
user.value = null
}
return {
user,
isLoggedIn,
login,
logout
}
}
// Usage in any component (auto-imported!)
<script setup>
const { user, isLoggedIn, logout } = useAuth()
</script>
useRouter & useRoute
<script setup>
const router = useRouter()
const route = useRoute()
// Navigation
const goToPost = (id) => {
router.push(`/blog/${id}`)
// or router.push({ path: '/blog', query: { id } })
}
// Access params/query
console.log(route.params.id)
console.log(route.query.sort)
</script>
State Management
Manage global state with useState or Pinia.
useState - Built-in
<!-- composables/useCounter.ts -->
export const useCounter = () => {
const count = useState('counter', () => 0)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
<!-- Any component -->
<script setup>
const { count, increment } = useCounter()
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
</div>
</template>
Pinia Store
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const isLoggedIn = computed(() => !!user.value)
async function login(credentials) {
const data = await $fetch('/api/login', {
method: 'POST',
body: credentials
})
user.value = data.user
}
function logout() {
user.value = null
}
return { user, isLoggedIn, login, logout }
})
// Usage
<script setup>
const userStore = useUserStore()
</script>
Tip: Use
useState for simple state, Pinia for complex state management with devtools.
Route Middleware
Run code before navigating to routes.
Named Middleware
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isLoggedIn } = useAuth()
if (!isLoggedIn.value) {
return navigateTo('/login')
}
})
// pages/dashboard.vue
<script setup>
definePageMeta({
middleware: 'auth'
})
</script>
Global Middleware
// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
// Runs on every route change
console.log('Navigating to:', to.path)
})
Inline Middleware
<script setup>
definePageMeta({
middleware: [
'auth',
function (to, from) {
// Inline middleware logic
const isAdmin = useState('isAdmin')
if (!isAdmin.value) {
return abortNavigation()
}
}
]
})
</script>
Server Routes & API
Create full-stack applications with server routes in the server/ directory.
API Route
// server/api/users.ts
export default defineEventHandler(async (event) => {
const users = await prisma.user.findMany()
return users
})
// Access at: /api/users
Dynamic API Route
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const user = await prisma.user.findUnique({
where: { id: parseInt(id) }
})
if (!user) {
throw createError({
statusCode: 404,
message: 'User not found'
})
}
return user
})
// Access at: /api/users/123
POST Request
// 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
})
Server Middleware
// server/middleware/auth.ts
export default defineEventHandler((event) => {
const token = getHeader(event, 'authorization')
if (!token) {
throw createError({
statusCode: 401,
message: 'Unauthorized'
})
}
// Verify token...
event.context.user = decodedUser
})
SEO & Meta Tags
Manage meta tags with useHead and useSeoMeta.
Basic Meta Tags
<script setup>
useHead({
title: 'My Nuxt App',
meta: [
{ name: 'description', content: 'My amazing Nuxt application' },
{ property: 'og:title', content: 'My Nuxt App' },
{ property: 'og:image', content: '/og-image.jpg' }
],
link: [
{ rel: 'icon', type: 'image/png', href: '/favicon.png' }
]
})
</script>
useSeoMeta - Type-safe
<script setup>
useSeoMeta({
title: 'My Nuxt App',
description: 'My amazing Nuxt application',
ogTitle: 'My Nuxt App',
ogDescription: 'My amazing Nuxt application',
ogImage: 'https://example.com/og-image.jpg',
twitterCard: 'summary_large_image',
})
</script>
Dynamic Meta
<script setup>
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)
useHead({
title: () => post.value?.title,
meta: [
{
name: 'description',
content: () => post.value?.excerpt
}
]
})
</script>
Global Meta (nuxt.config.ts)
export default defineNuxtConfig({
app: {
head: {
title: 'My Nuxt App',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: 'description', content: 'My amazing app' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
}
}
})
SEO Friendly: Meta tags are rendered on the server for perfect SEO!