Back to Posts
Frontend #Nuxt.js #Vue.js #SSR

Getting Started with Nuxt 3

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

5 min read
Getting Started with Nuxt 3

Getting Started with Nuxt 3

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.


Table of Contents

  1. Why Choose Nuxt 3?
  2. Installation and Setup
  3. Project Structure
  4. File-System Routing
  5. Pages and Layouts
  6. Components and Auto-Imports
  7. Composables and Utils
  8. Data Fetching
  9. Server Routes and API
  10. State Management
  11. SEO and Meta Tags
  12. Rendering Modes
  13. Modules and Configuration
  14. Deployment
  15. Best Practices

Why Choose Nuxt 3?

Understanding Nuxt 3's advantages helps you leverage its full potential.

Vue 3 at the Core

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.

Modern Build Tools

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.

Hybrid Rendering

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.

Developer Experience

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.

Production Ready

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.


Installation and Setup

Let's create your first Nuxt 3 project.

Prerequisites

Ensure you have Node.js installed (version 16.10 or higher):

node --version
# Should show v16.10 or higher

Create a New Project

# 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

Start Development Server

npm run dev

# Server starts at http://localhost:3000

Project Structure Overview

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

Your First Page

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>

Project Structure

Understanding the Nuxt 3 project structure helps you organize code effectively.

Core Directories

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

File-System Routing

Nuxt 3 automatically generates routes from your pages/ directory.

Basic Routes

<!-- 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>

Nested Routes

pages/
├── users/
│   ├── index.vue     # /users
│   ├── profile.vue   # /users/profile
│   └── settings.vue  # /users/settings

Dynamic Routes

<!-- 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>

Catch-All Routes

<!-- 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>

Route Metadata

<!-- pages/admin.vue -->
<script setup lang="ts">
definePageMeta({
  layout: 'admin',
  middleware: 'auth',
  title: 'Admin Dashboard'
})
</script>

<template>
  <div>Admin content</div>
</template>

Pages and Layouts

Create reusable layouts and structured pages.

Default Layout

<!-- 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>&copy; 2025 My App</p>
    </footer>
  </div>
</template>

<style scoped>
.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

main {
  flex: 1;
  padding: 2rem;
}
</style>

Custom Layouts

<!-- 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>

Using Layouts in Pages

<!-- 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 as Root Layout

<!-- app.vue -->
<template>
  <div id="app">
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>

<script setup lang="ts">
// Global setup, plugins, etc.
</script>

Components and Auto-Imports

Nuxt 3 automatically imports components from the components/ directory.

Component Auto-Import

<!-- 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>

Nested Components

components/
├── ui/
│   ├── Button.vue    # <UiButton />
│   └── Card.vue      # <UiCard />
└── features/
    └── UserCard.vue  # <FeaturesUserCard />
<template>
  <div>
    <UiButton>Click</UiButton>
    <UiCard>
      <FeaturesUserCard />
    </UiCard>
  </div>
</template>

Dynamic Components

<template>
  <component :is="currentComponent" />
</template>

<script setup lang="ts">
const currentComponent = ref('AppButton')

// Switch components
function switchComponent() {
  currentComponent.value = 'AppCard'
}
</script>

Client-Only Components

<template>
  <div>
    <ClientOnly>
      <MapComponent />
      <template #fallback>
        <p>Loading map...</p>
      </template>
    </ClientOnly>
  </div>
</template>

Lazy Components

<template>
  <div>
    <!-- Lazy load component -->
    <LazyAppHeavyComponent v-if="showComponent" />
  </div>
</template>

Composables and Utils

Create reusable logic with auto-imported composables.

Creating 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
  }
}

Using Composables

<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>

Auth Composable Example

// 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

// 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>

Data Fetching

Nuxt 3 provides powerful data fetching composables.

useFetch

<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>

useAsyncData

<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>

$fetch

<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>

Lazy Fetching

<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>

Query Parameters

<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>

Server Routes and API

Build API endpoints directly in your Nuxt app.

Basic API Route

// server/api/hello.get.ts
export default defineEventHandler((event) => {
  return {
    message: 'Hello from Nuxt API!'
  }
})

// GET /api/hello

CRUD Operations

// 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 }
})

Query Parameters

// 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: []
  }
})

Middleware

// 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
})

State Management

Manage global state with built-in composables.

useState

// 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
  }
}

Using State

<!-- 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>

Pinia Integration

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
    }
  }
})

SEO and Meta Tags

Optimize your app for search engines.

useHead

<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>

useSeoMeta

<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>

Dynamic Meta Tags

<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>

Rendering Modes

Choose the right rendering strategy for your needs.

Universal Rendering (SSR)

Default mode, renders on server then hydrates on client:

// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true  // Default
})

Client-Side Rendering (SPA)

// nuxt.config.ts
export default defineNuxtConfig({
  ssr: false
})

Static Site Generation

# Build static site
npm run generate

# Output in .output/public/

Hybrid Rendering

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>

Modules and Configuration

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

Configuration

// 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 *;'
        }
      }
    }
  }
})

Deployment

Deploy your Nuxt 3 application to various platforms.

Build for Production

# Build application
npm run build

# Preview production build
npm run preview

Vercel

# Install Vercel CLI
npm i -g vercel

# Deploy
vercel

Netlify

# netlify.toml
[build]
  command = "npm run build"
  publish = ".output/public"

Node.js Server

# Build
npm run build

# Start server
node .output/server/index.mjs

Docker

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"]

Best Practices

Use TypeScript

<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
}

const users = ref<User[]>([])
</script>

Optimize Images

<template>
  <NuxtImg
    src="/images/hero.jpg"
    width="800"
    height="600"
    format="webp"
    loading="lazy"
  />
</template>

Error Handling

<script setup lang="ts">
const { data, error } = await useFetch('/api/data')

if (error.value) {
  throw createError({
    statusCode: 500,
    message: 'Failed to load data'
  })
}
</script>

Code Splitting

<script setup lang="ts">
// Lazy load heavy component
const HeavyComponent = defineAsyncComponent(() =>
  import('~/components/HeavyComponent.vue')
)
</script>

Conclusion

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.

Key Takeaways

  • Zero Configuration: Start building immediately with sensible defaults
  • File-System Routing: Automatic route generation from your file structure
  • Auto-Imports: Components, composables, and utilities automatically available
  • Hybrid Rendering: Mix SSR, SSG, and SPA modes per route
  • Full-Stack: Build APIs and server routes alongside your frontend

Next Steps

  1. Explore Modules: Add Tailwind, Content, Image optimization
  2. Build a Project: Create a real application to solidify concepts
  3. Learn Nitro: Understand the server engine powering Nuxt
  4. Optimize Performance: Lazy loading, code splitting, caching
  5. Deploy: Get your app online with Vercel, Netlify, or Node.js

Additional Resources

Nuxt 3 makes web development delightful. Start building amazing applications today!

Back to All Posts