Getting Started with Nuxt 3
Learn how to set up a new Nuxt 3 project and understand its core concepts.
Learn how to set up a new Nuxt 3 project and understand its core concepts.

Introduction: The evolution of Vue.js brought us an incredible framework, but building production-ready applications with server-side rendering, routing, and state management required assembling various tools and libraries. Nuxt 3 changes everything. Built from the ground up with Vue 3, TypeScript, and Vite, Nuxt 3 provides a complete framework for building modern web applications with zero configuration. Whether you're building a static blog, a dynamic web app, or a full-stack application with server routes, Nuxt 3 gives you the tools and performance you need to succeed.
Nuxt 3 is an open-source framework making web development intuitive and powerful. Create performant and production-grade full-stack web apps and websites with confidence. Nuxt leverages Vue components, composables, and the latest web standards to deliver exceptional developer experience and end-user performance. With features like file-system routing, automatic imports, server-side rendering, and hybrid rendering modes, Nuxt 3 simplifies complex workflows while maintaining flexibility and power.
Understanding Nuxt 3's advantages helps you leverage its full potential.
Nuxt 3 is built on Vue 3, bringing all the latest features:
Composition API: Better code organization, improved TypeScript support, and enhanced reusability.
Performance: Smaller bundle sizes, faster rendering, and optimized reactivity system.
Script Setup: Cleaner component syntax with less boilerplate.
Suspense: Built-in loading state management for async components.
Vite: Lightning-fast hot module replacement during development and optimized production builds.
Nitro: Universal server engine that works anywhere—Node.js, serverless, edge workers.
TypeScript: First-class TypeScript support out of the box, no configuration needed.
Choose the best rendering strategy for each page:
SSR (Server-Side Rendering): Dynamic content with SEO benefits.
SSG (Static Site Generation): Pre-render pages at build time for maximum performance.
ISR (Incremental Static Regeneration): Update static pages without rebuilding the entire site.
CSR (Client-Side Rendering): SPA-style rendering when needed.
Zero Configuration: Start building immediately with sensible defaults.
Auto-Imports: Components, composables, and utilities are automatically imported.
File-System Routing: Routes are generated from your file structure.
Hot Module Replacement: See changes instantly without full page reloads.
DevTools: Built-in debugging tools with Vue DevTools integration.
SEO Optimized: Server-side rendering and meta tag management out of the box.
Performance: Automatic code splitting, lazy loading, and optimized builds.
Security: Built-in protection against common vulnerabilities.
Deployment Flexibility: Deploy anywhere—Vercel, Netlify, AWS, Node.js servers.
Let's create your first Nuxt 3 project.
Ensure you have Node.js installed (version 16.10 or higher):
node --version
# Should show v16.10 or higher
# Using npx (recommended)
npx nuxi@latest init my-nuxt-app
# Or using pnpm
pnpm dlx nuxi@latest init my-nuxt-app
# Or using yarn
yarn dlx nuxi@latest init my-nuxt-app
cd my-nuxt-app
# Install dependencies
npm install
# Or with pnpm
pnpm install
# Or with yarn
yarn install
npm run dev
# Server starts at http://localhost:3000
After creation, you'll see this structure:
my-nuxt-app/
├── .nuxt/ # Auto-generated build files
├── node_modules/ # Dependencies
├── public/ # Static assets
├── server/ # Server-side code
│ └── api/ # API routes
├── .gitignore
├── app.vue # Root component
├── nuxt.config.ts # Nuxt configuration
├── package.json
└── tsconfig.json # TypeScript config
The default app.vue is your entry point:
<!-- app.vue -->
<template>
<div>
<h1>Welcome to Nuxt 3!</h1>
<p>Start building your application</p>
</div>
</template>
<script setup lang="ts">
// Your setup code here
</script>
<style scoped>
h1 {
color: #00dc82;
}
</style>
Understanding the Nuxt 3 project structure helps you organize code effectively.
pages/ - File-system based routing:
pages/
├── index.vue # /
├── about.vue # /about
├── users/
│ ├── index.vue # /users
│ └── [id].vue # /users/:id
└── blog/
└── [...slug].vue # /blog/* (catch-all)
components/ - Auto-imported Vue components:
components/
├── AppHeader.vue # <AppHeader />
├── AppFooter.vue # <AppFooter />
├── ui/
│ ├── Button.vue # <UiButton />
│ └── Card.vue # <UiCard />
└── features/
└── UserCard.vue # <FeaturesUserCard />
layouts/ - Reusable page layouts:
layouts/
├── default.vue # Default layout
├── admin.vue # Admin layout
└── blank.vue # Minimal layout
composables/ - Auto-imported composable functions:
composables/
├── useAuth.ts # useAuth()
├── useFetch.ts # useFetch()
└── useState.ts # useState()
server/ - Server-side code and API routes:
server/
├── api/
│ ├── users.get.ts # GET /api/users
│ ├── users.post.ts # POST /api/users
│ └── auth/
│ └── login.post.ts # POST /api/auth/login
├── routes/
│ └── sitemap.xml.ts # GET /sitemap.xml
└── middleware/
└── auth.ts
public/ - Static files served at root:
public/
├── favicon.ico
├── robots.txt
└── images/
└── logo.png # /images/logo.png
assets/ - Assets processed by build tool:
assets/
├── css/
│ └── main.css
├── images/
│ └── banner.jpg
└── fonts/
└── custom.woff2
plugins/ - Plugins to run before app initialization:
plugins/
├── api.ts # API configuration
├── gtag.client.ts # Client-only plugin
└── logger.server.ts # Server-only plugin
middleware/ - Route middleware:
middleware/
├── auth.ts # Authentication
└── guest.ts # Guest-only routes
Nuxt 3 automatically generates routes from your pages/ directory.
<!-- pages/index.vue → / -->
<template>
<div>
<h1>Home Page</h1>
</div>
</template>
<!-- pages/about.vue → /about -->
<template>
<div>
<h1>About Page</h1>
</div>
</template>
<!-- pages/contact.vue → /contact -->
<template>
<div>
<h1>Contact Page</h1>
</div>
</template>
pages/
├── users/
│ ├── index.vue # /users
│ ├── profile.vue # /users/profile
│ └── settings.vue # /users/settings
<!-- pages/users/[id].vue → /users/:id -->
<template>
<div>
<h1>User: {{ route.params.id }}</h1>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
</script>
<!-- pages/blog/[year]/[month]/[slug].vue → /blog/:year/:month/:slug -->
<template>
<div>
<h1>{{ route.params.year }}/{{ route.params.month }}/{{ route.params.slug }}</h1>
</div>
</template>
<!-- pages/blog/[...slug].vue → /blog/* -->
<template>
<div>
<h1>Blog: {{ route.params.slug.join('/') }}</h1>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
// /blog/2025/12/my-post → ['2025', '12', 'my-post']
</script>
<template>
<div>
<!-- NuxtLink component -->
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/about">About</NuxtLink>
<NuxtLink :to="`/users/${userId}`">User Profile</NuxtLink>
<!-- Programmatic navigation -->
<button @click="goToAbout">Go to About</button>
</div>
</template>
<script setup lang="ts">
const router = useRouter()
const userId = ref(123)
function goToAbout() {
router.push('/about')
}
// Navigation with params
function goToUser(id: number) {
router.push({
path: `/users/${id}`,
query: { tab: 'profile' }
})
}
</script>
<!-- pages/admin.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'admin',
middleware: 'auth',
title: 'Admin Dashboard'
})
</script>
<template>
<div>Admin content</div>
</template>
Create reusable layouts and structured pages.
<!-- layouts/default.vue -->
<template>
<div class="app">
<header>
<nav>
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/about">About</NuxtLink>
<NuxtLink to="/blog">Blog</NuxtLink>
</nav>
</header>
<main>
<slot /> <!-- Page content goes here -->
</main>
<footer>
<p>© 2025 My App</p>
</footer>
</div>
</template>
<style scoped>
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
padding: 2rem;
}
</style>
<!-- layouts/admin.vue -->
<template>
<div class="admin-layout">
<aside class="sidebar">
<nav>
<NuxtLink to="/admin">Dashboard</NuxtLink>
<NuxtLink to="/admin/users">Users</NuxtLink>
<NuxtLink to="/admin/settings">Settings</NuxtLink>
</nav>
</aside>
<div class="content">
<slot />
</div>
</div>
</template>
<!-- layouts/blank.vue -->
<template>
<div>
<slot />
</div>
</template>
<!-- pages/admin/index.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'admin'
})
</script>
<template>
<div>
<h1>Admin Dashboard</h1>
</div>
</template>
<!-- pages/login.vue -->
<script setup lang="ts">
definePageMeta({
layout: 'blank'
})
</script>
<template>
<div class="login-page">
<h1>Login</h1>
</div>
</template>
<!-- app.vue -->
<template>
<div id="app">
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
<script setup lang="ts">
// Global setup, plugins, etc.
</script>
Nuxt 3 automatically imports components from the components/ directory.
<!-- components/AppButton.vue -->
<template>
<button class="btn" :class="variant">
<slot />
</button>
</template>
<script setup lang="ts">
defineProps<{
variant?: 'primary' | 'secondary'
}>()
</script>
<!-- pages/index.vue -->
<template>
<div>
<!-- No import needed! -->
<AppButton variant="primary">Click Me</AppButton>
</div>
</template>
components/
├── ui/
│ ├── Button.vue # <UiButton />
│ └── Card.vue # <UiCard />
└── features/
└── UserCard.vue # <FeaturesUserCard />
<template>
<div>
<UiButton>Click</UiButton>
<UiCard>
<FeaturesUserCard />
</UiCard>
</div>
</template>
<template>
<component :is="currentComponent" />
</template>
<script setup lang="ts">
const currentComponent = ref('AppButton')
// Switch components
function switchComponent() {
currentComponent.value = 'AppCard'
}
</script>
<template>
<div>
<ClientOnly>
<MapComponent />
<template #fallback>
<p>Loading map...</p>
</template>
</ClientOnly>
</div>
</template>
<template>
<div>
<!-- Lazy load component -->
<LazyAppHeavyComponent v-if="showComponent" />
</div>
</template>
Create reusable logic with auto-imported composables.
// composables/useCounter.ts
export const useCounter = () => {
const count = ref(0)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const reset = () => {
count.value = 0
}
return {
count,
increment,
decrement,
reset
}
}
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup lang="ts">
// Auto-imported!
const { count, increment, decrement, reset } = useCounter()
</script>
// composables/useAuth.ts
export const useAuth = () => {
const user = useState<User | null>('user', () => null)
const token = useCookie('auth-token')
const login = async (email: string, password: string) => {
const { data, error } = await useFetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
if (data.value) {
user.value = data.value.user
token.value = data.value.token
}
return { data, error }
}
const logout = async () => {
await $fetch('/api/auth/logout', { method: 'POST' })
user.value = null
token.value = null
}
const isAuthenticated = computed(() => !!user.value)
return {
user,
login,
logout,
isAuthenticated
}
}
// utils/formatDate.ts
export const formatDate = (date: Date | string) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(new Date(date))
}
// Auto-imported in components
// <p>{{ formatDate(post.createdAt) }}</p>
Nuxt 3 provides powerful data fetching composables.
<template>
<div>
<div v-if="pending">Loading...</div>
<div v-else-if="error">{{ error.message }}</div>
<div v-else>
<div v-for="user in data" :key="user.id">
{{ user.name }}
</div>
</div>
<button @click="refresh">Refresh</button>
</div>
</template>
<script setup lang="ts">
const { data, pending, error, refresh } = await useFetch('/api/users')
</script>
<script setup lang="ts">
const { data: posts } = await useAsyncData('posts', () =>
$fetch('/api/posts')
)
// With options
const { data: user } = await useAsyncData(
'user',
() => $fetch(`/api/users/${route.params.id}`),
{
watch: [() => route.params.id],
server: true,
lazy: false
}
)
</script>
<script setup lang="ts">
async function createPost() {
try {
const post = await $fetch('/api/posts', {
method: 'POST',
body: {
title: 'New Post',
content: 'Post content'
}
})
console.log('Created:', post)
} catch (error) {
console.error('Failed:', error)
}
}
</script>
<script setup lang="ts">
const { data, pending, refresh } = await useLazyFetch('/api/posts')
// Data is null initially, loads in background
</script>
<template>
<div>
<div v-if="!data">Loading in background...</div>
<div v-else>{{ data }}</div>
</div>
</template>
<script setup lang="ts">
const page = ref(1)
const search = ref('')
const { data: results } = await useFetch('/api/search', {
query: {
page,
q: search
},
watch: [page, search]
})
</script>
Build API endpoints directly in your Nuxt app.
// server/api/hello.get.ts
export default defineEventHandler((event) => {
return {
message: 'Hello from Nuxt API!'
}
})
// GET /api/hello
// server/api/users/index.get.ts
export default defineEventHandler(async (event) => {
// Fetch from database
const users = await db.users.findMany()
return users
})
// server/api/users/index.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validate
if (!body.name || !body.email) {
throw createError({
statusCode: 400,
message: 'Name and email required'
})
}
// Create user
const user = await db.users.create({
data: body
})
return user
})
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const user = await db.users.findUnique({
where: { id: Number(id) }
})
if (!user) {
throw createError({
statusCode: 404,
message: 'User not found'
})
}
return user
})
// server/api/users/[id].put.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const body = await readBody(event)
const user = await db.users.update({
where: { id: Number(id) },
data: body
})
return user
})
// server/api/users/[id].delete.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
await db.users.delete({
where: { id: Number(id) }
})
return { success: true }
})
// server/api/search.get.ts
export default defineEventHandler((event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 10
const search = query.q as string
// Perform search
return {
page,
limit,
results: []
}
})
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
const token = getCookie(event, 'auth-token')
if (!token) {
return // Continue without auth
}
// Verify token and set user context
const user = await verifyToken(token)
event.context.user = user
})
Manage global state with built-in composables.
// composables/useStore.ts
export const useStore = () => {
const user = useState<User | null>('user', () => null)
const cart = useState<CartItem[]>('cart', () => [])
const theme = useState<'light' | 'dark'>('theme', () => 'light')
return {
user,
cart,
theme
}
}
<!-- components/Header.vue -->
<script setup lang="ts">
const { user, theme } = useStore()
</script>
<template>
<header :class="theme">
<p v-if="user">Welcome, {{ user.name }}</p>
</header>
</template>
<!-- pages/profile.vue -->
<script setup lang="ts">
const { user } = useStore()
// Same state instance!
</script>
npm install pinia @pinia/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt']
})
// stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: null as User | null,
token: null as string | null
}),
getters: {
isAuthenticated: (state) => !!state.user
},
actions: {
async login(email: string, password: string) {
const { data } = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
this.user = data.user
this.token = data.token
},
logout() {
this.user = null
this.token = null
}
}
})
Optimize your app for search engines.
<script setup lang="ts">
useHead({
title: 'My Page Title',
meta: [
{ name: 'description', content: 'Page description' },
{ property: 'og:title', content: 'My Page Title' },
{ property: 'og:description', content: 'Page description' },
{ property: 'og:image', content: '/og-image.jpg' }
],
link: [
{ rel: 'canonical', href: 'https://example.com/page' }
]
})
</script>
<script setup lang="ts">
useSeoMeta({
title: 'My Amazing Site',
ogTitle: 'My Amazing Site',
description: 'This is my amazing site description',
ogDescription: 'This is my amazing site description',
ogImage: 'https://example.com/image.png',
twitterCard: 'summary_large_image'
})
</script>
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)
useHead({
title: () => post.value?.title,
meta: [
{
name: 'description',
content: () => post.value?.excerpt
},
{
property: 'og:image',
content: () => post.value?.image
}
]
})
</script>
Choose the right rendering strategy for your needs.
Default mode, renders on server then hydrates on client:
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true // Default
})
// nuxt.config.ts
export default defineNuxtConfig({
ssr: false
})
# Build static site
npm run generate
# Output in .output/public/
Mix rendering modes per route:
<script setup lang="ts">
defineRouteRules({
'/admin/**': { ssr: false }, // SPA
'/blog/**': { static: true }, // SSG
'/api/**': { cors: true }, // API
'/products/**': { swr: 3600 } // ISR (1 hour)
})
</script>
Extend Nuxt with modules and custom configuration.
# Tailwind CSS
npm install -D @nuxtjs/tailwindcss
# Content (Markdown, JSON, etc.)
npm install @nuxt/content
# Image optimization
npm install -D @nuxt/image
# PWA
npm install @vite-pwa/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@nuxtjs/tailwindcss',
'@nuxt/content',
'@nuxt/image'
],
app: {
head: {
title: 'My App',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
]
}
},
runtimeConfig: {
apiSecret: process.env.API_SECRET,
public: {
apiBase: process.env.API_BASE
}
},
css: ['~/assets/css/main.css'],
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: '@use "~/assets/scss/variables.scss" as *;'
}
}
}
}
})
Deploy your Nuxt 3 application to various platforms.
# Build application
npm run build
# Preview production build
npm run preview
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# netlify.toml
[build]
command = "npm run build"
publish = ".output/public"
# Build
npm run build
# Start server
node .output/server/index.mjs
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]
<script setup lang="ts">
interface User {
id: number
name: string
email: string
}
const users = ref<User[]>([])
</script>
<template>
<NuxtImg
src="/images/hero.jpg"
width="800"
height="600"
format="webp"
loading="lazy"
/>
</template>
<script setup lang="ts">
const { data, error } = await useFetch('/api/data')
if (error.value) {
throw createError({
statusCode: 500,
message: 'Failed to load data'
})
}
</script>
<script setup lang="ts">
// Lazy load heavy component
const HeavyComponent = defineAsyncComponent(() =>
import('~/components/HeavyComponent.vue')
)
</script>
Nuxt 3 revolutionizes Vue.js development by providing a complete, production-ready framework that handles routing, data fetching, server-side rendering, and deployment with minimal configuration. Its file-system conventions, auto-imports, and powerful composables make development intuitive and enjoyable.
Whether you're building a simple blog, a complex e-commerce platform, or a full-stack application, Nuxt 3 provides the tools and flexibility you need. The framework's hybrid rendering capabilities ensure you can choose the best strategy for each page, optimizing for both performance and SEO.
Nuxt 3 makes web development delightful. Start building amazing applications today!