DevToolBoxGRATIS
Blog

Nuxt 3: The Complete Guide to the Vue.js Full-Stack Framework

24 min readoleh DevToolBox Team

Nuxt 3 is a powerful full-stack framework built on Vue 3 and Nitro, offering file-based routing, auto-imports, server-side rendering, and a rich module ecosystem. Whether you are building a content site, a SaaS dashboard, or a full-stack application, Nuxt 3 provides the tools to ship fast with minimal configuration. This guide covers everything from project setup to production deployment.

TL;DR: Nuxt 3 is a full-stack Vue framework powered by Nitro. It features file-based routing, auto-imports, built-in SSR/SSG/ISR, composables like useState and useFetch, server routes, middleware, layouts, and a module ecosystem. Deploy to Vercel, Cloudflare Workers, or Node.js with zero config.

Key Takeaways

  • Nuxt 3 uses file-based routing under the pages/ directory with dynamic segments, catch-all routes, and nested layouts
  • The Nitro server engine powers server routes, API endpoints, and edge deployment with universal output
  • Auto-imports eliminate boilerplate for Vue APIs, composables, and utility functions
  • Built-in composables like useFetch, useAsyncData, and useState handle data fetching and state management
  • Middleware (route guards), layouts, and plugins provide structured application architecture
  • Nuxt modules extend functionality: content, image, auth, SEO, and 200+ community modules
  • Deploy with a single command to Vercel, Cloudflare Workers, Netlify, or any Node.js server
  • Nuxt 3 supports SSR, SSG, ISR, SPA, and hybrid rendering per route

Getting Started with Nuxt 3

Creating a new Nuxt 3 project takes a single command. The CLI scaffolds a minimal project with TypeScript support, dev server, and hot module replacement configured out of the box.

# Create a new Nuxt 3 project
npx nuxi@latest init my-nuxt-app

# Navigate into the project
cd my-nuxt-app

# Install dependencies
npm install

# Start development server (http://localhost:3000)
npm run dev

# Build for production
npm run build

# Preview production build
npm run preview

Project Structure

Nuxt uses a convention-based directory structure. Each directory has a specific purpose and Nuxt auto-discovers files placed in them.

my-nuxt-app/
  nuxt.config.ts          # Nuxt configuration
  app.vue                  # Root component
  pages/                   # File-based routing
    index.vue              # / route
    about.vue              # /about route
    blog/
      index.vue            # /blog route
      [slug].vue           # /blog/:slug route
  components/              # Auto-imported components
    AppHeader.vue
    AppFooter.vue
  composables/             # Auto-imported composables
    useAuth.ts
    useApi.ts
  layouts/                 # Page layouts
    default.vue
    admin.vue
  middleware/              # Route middleware
    auth.ts
  plugins/                 # App plugins
    analytics.ts
  server/                  # Server routes (Nitro)
    api/
      users.get.ts         # GET /api/users
      users.post.ts        # POST /api/users
    middleware/
      log.ts
  public/                  # Static assets
  assets/                  # Build-processed assets

File-Based Routing

Nuxt generates routes automatically from Vue files in the pages/ directory. The file path maps directly to the URL path, with support for dynamic parameters, catch-all routes, and nested routing.

<!-- pages/index.vue -->
<template>
  <div>
    <h1>Welcome to Nuxt 3</h1>
    <NuxtLink to="/about">About</NuxtLink>
    <NuxtLink to="/blog">Blog</NuxtLink>
  </div>
</template>

Dynamic Routes

Dynamic segments are defined with square brackets in the filename. The parameter is available via the useRoute composable.

<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
// Access the dynamic route parameter
const route = useRoute()
const slug = route.params.slug

// Fetch blog post data
const { data: post } = await useFetch(
  `/api/blog/\${slug}`
)
</script>

<template>
  <article v-if="post">
    <h1>{{ post.title }}</h1>
    <div v-html="post.content" />
  </article>
</template>

Catch-All and Optional Parameters

Use the spread syntax for catch-all routes and question mark for optional parameters.

# Catch-all route: matches /docs/a, /docs/a/b, /docs/a/b/c
pages/docs/[...slug].vue

# Optional parameter: matches /users and /users/123
pages/users/[[id]].vue

# Route mapping examples:
# pages/index.vue            -> /
# pages/about.vue            -> /about
# pages/blog/index.vue       -> /blog
# pages/blog/[slug].vue      -> /blog/:slug
# pages/blog/[...slug].vue   -> /blog/*
# pages/users-[group]/[id].vue -> /users-:group/:id

Nested Routes

Create nested layouts by placing a Vue file and a matching directory side by side. The parent file uses NuxtPage to render child routes.

<!-- pages/dashboard.vue (parent layout) -->
<template>
  <div>
    <nav>
      <NuxtLink to="/dashboard">Overview</NuxtLink>
      <NuxtLink to="/dashboard/analytics">Analytics</NuxtLink>
      <NuxtLink to="/dashboard/settings">Settings</NuxtLink>
    </nav>
    <!-- Child routes render here -->
    <NuxtPage />
  </div>
</template>

<!-- pages/dashboard/index.vue -->
<template>
  <div>Dashboard Overview</div>
</template>

<!-- pages/dashboard/analytics.vue -->
<template>
  <div>Analytics Page</div>
</template>

Server Routes and API Endpoints

Nuxt 3 includes a built-in server powered by Nitro. Define API endpoints by creating files in the server/ directory. Server routes have access to the H3 event object for request handling.

Defining API Endpoints

Create files in server/api/ or server/routes/ to define endpoints. The file name becomes the route path.

// server/api/users.get.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  const page = Number(query.page) || 1
  return {
    users: await fetchUsersFromDB(page),
    total: await countUsers(),
  }
})

// server/api/users.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  if (!body.email || !body.name) {
    throw createError({
      statusCode: 400,
      statusMessage: "Name and email are required",
    })
  }
  return { user: await createUser(body) }
})

// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, "id")
  const user = await getUserById(id)
  if (!user) {
    throw createError({ statusCode: 404, statusMessage: "User not found" })
  }
  return { user }
})

Server Middleware

Server middleware runs on every request before routes are matched. Use it for authentication, logging, or CORS headers.

// server/middleware/auth.ts
export default defineEventHandler((event) => {
  const token = getHeader(event, "authorization")

  // Skip auth for public routes
  if (event.path.startsWith("/api/public")) return

  if (!token) {
    throw createError({
      statusCode: 401,
      statusMessage: "Unauthorized",
    })
  }

  // Attach user info to the event context
  event.context.user = verifyToken(token)
})

// server/middleware/logger.ts
export default defineEventHandler((event) => {
  console.log(
    `[\${new Date().toISOString()}] \${event.method} \${event.path}`
  )
})

Database Access in Server Routes

Server routes run in a Node.js-compatible environment, giving you access to databases, file systems, and any server-side library.

// server/utils/db.ts
import { drizzle } from "drizzle-orm/node-postgres"
import { Pool } from "pg"

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
})

export const db = drizzle(pool)

// server/api/posts.get.ts
import { db } from "~~/server/utils/db"
import { posts } from "~~/server/db/schema"

export default defineEventHandler(async () => {
  return await db.select().from(posts).orderBy(posts.createdAt)
})

Built-In Composables

Nuxt provides composables for common tasks like data fetching, state management, and navigation. These are auto-imported and SSR-friendly.

useFetch

The primary composable for data fetching. It handles SSR hydration, caching, and provides reactive data, error, and pending states.

<script setup lang="ts">
// Basic usage - returns reactive data, pending, error, refresh
const { data, pending, error, refresh } = await useFetch("/api/posts")

// With options: query params, transform, caching
const { data: users } = await useFetch("/api/users", {
  query: { page: 1, limit: 20 },
  transform: (response) => response.users,
  key: "users-page-1",
})

// Lazy fetch (non-blocking, loads after navigation)
const { data: comments } = useLazyFetch("/api/comments")

// Watch reactive source and refetch automatically
const page = ref(1)
const { data: items } = await useFetch("/api/items", {
  query: { page },
  watch: [page],
})
</script>

<template>
  <div v-if="pending">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <ul v-else>
    <li v-for="post in data" :key="post.id">{{ post.title }}</li>
  </ul>
</template>

useAsyncData

For custom async operations that are not simple HTTP requests. It wraps any async function with SSR hydration support.

<script setup lang="ts">
// Fetch from an external service
const { data: weather } = await useAsyncData(
  "weather",  // unique key for caching
  () => $fetch("https://api.weather.com/current", {
    params: { city: "london" }
  })
)

// Combine multiple async operations
const { data: dashboard } = await useAsyncData(
  "dashboard",
  async () => {
    const [users, orders, revenue] = await Promise.all([
      $fetch("/api/users/count"),
      $fetch("/api/orders/recent"),
      $fetch("/api/revenue/monthly"),
    ])
    return { users, orders, revenue }
  }
)
</script>

useState

A shared, SSR-friendly reactive state composable. Unlike Vue ref, useState is serialized during SSR and shared across components.

// composables/useCounter.ts
export const useCounter = () => {
  const count = useState("counter", () => 0)
  const increment = () => count.value++
  const decrement = () => count.value--
  return { count, increment, decrement }
}

// composables/useAuth.ts
export const useAuth = () => {
  const user = useState("auth-user", () => null)
  const isLoggedIn = computed(() => !!user.value)

  const login = async (email: string, password: string) => {
    const data = await $fetch("/api/auth/login", {
      method: "POST", body: { email, password },
    })
    user.value = data.user
  }

  const logout = () => {
    user.value = null
    navigateTo("/login")
  }
  return { user, isLoggedIn, login, logout }
}

useHead and useSeoMeta

Manage document head metadata reactively. useSeoMeta provides type-safe SEO meta tag management.

<script setup lang="ts">
// Basic head management
useHead({
  title: "My Page Title",
  meta: [
    { name: "description", content: "Page description here" },
  ],
  link: [
    { rel: "canonical", href: "https://example.com/page" },
  ],
})

// Type-safe SEO meta tags
useSeoMeta({
  title: "Nuxt 3 Guide",
  ogTitle: "Nuxt 3 Guide",
  description: "Complete guide to building with Nuxt 3",
  ogDescription: "Complete guide to building with Nuxt 3",
  ogImage: "https://example.com/og.png",
  twitterCard: "summary_large_image",
})
</script>

Nitro Server Engine

Nitro is the server engine that powers Nuxt 3. It provides a universal runtime that works across Node.js, Cloudflare Workers, Deno, Bun, and more. Nitro handles server routes, caching, storage, and output bundling.

Key Nitro Features

Nitro offers hot module replacement for server routes, automatic API route generation, built-in caching with defineCachedEventHandler, storage layer with useStorage, and cross-platform deployment presets.

Cached Server Routes

Use defineCachedEventHandler to cache server route responses. This is useful for expensive database queries or external API calls.

// server/api/popular-posts.get.ts
export default defineCachedEventHandler(
  async () => {
    const posts = await db.select().from(postsTable)
      .orderBy(desc(postsTable.views)).limit(20)
    return { posts }
  },
  { maxAge: 60 * 10, name: "popular-posts" }
)

Nitro Storage Layer

Nitro provides a universal storage layer that works across different drivers: memory, filesystem, Redis, Cloudflare KV, and more.

// nuxt.config.ts - configure storage
export default defineNuxtConfig({
  nitro: {
    storage: {
      redis: { driver: "redis", host: "localhost", port: 6379 },
      fs: { driver: "fs", base: "./data" },
    },
  },
})

// server/api/cache.ts - use storage
export default defineEventHandler(async () => {
  const storage = useStorage("redis")
  await storage.setItem("user:123", { name: "Alice", role: "admin" })
  const user = await storage.getItem("user:123")
  const keys = await storage.getKeys("user:")
  return { user, keys }
})

Auto-Imports

Nuxt automatically imports Vue APIs, composables, and utility functions. You never need to manually import ref, computed, useFetch, or navigateTo. This applies to your own composables in the composables/ directory as well.

<script setup lang="ts">
// All auto-imported - no import statements needed!
const count = ref(0)                          // Vue API
const doubled = computed(() => count.value * 2)
watch(count, (val) => console.log("Count:", val))

const route = useRoute()                      // Nuxt composables
const config = useRuntimeConfig()
const { data } = await useFetch("/api/data")
const goHome = () => navigateTo("/")           // Navigation
</script>

Custom Composables

Create composables in the composables/ directory and they are automatically available throughout your application.

// composables/useApi.ts
export const useApi = <T>(url: string, options = {}) => {
  const config = useRuntimeConfig()
  return useFetch<T>(url, {
    baseURL: config.public.apiBase,
    headers: {
      Authorization: `Bearer \${useAuth().token.value}`,
    },
    ...options,
  })
}

// composables/useLocalStorage.ts
export const useLocalStorage = <T>(key: string, defaultValue: T) => {
  const data = useState<T>(key, () => defaultValue)
  if (import.meta.client) {
    const stored = localStorage.getItem(key)
    if (stored) data.value = JSON.parse(stored)
    watch(data, (val) => {
      localStorage.setItem(key, JSON.stringify(val))
    }, { deep: true })
  }
  return data
}

Nuxt Modules

Modules extend Nuxt functionality with a plugin system. The Nuxt ecosystem includes over 200 modules for content management, authentication, image optimization, SEO, analytics, and more.

Popular Modules

ModulePurposePackage
Nuxt ContentFile-based CMS with Markdown, MDX, YAML@nuxt/content
Nuxt ImageAutomatic image optimization and resizing@nuxt/image
Nuxt UIComponent library with Tailwind CSS@nuxt/ui
Nuxt Auth UtilsSession-based authenticationnuxt-auth-utils
Nuxt SEOAll-in-one SEO toolkit@nuxtjs/seo
VueUseCollection of Vue composition utilities@vueuse/nuxt
PiniaState management for Vue@pinia/nuxt
Nuxt i18nInternationalization and localization@nuxtjs/i18n

Installing Modules

Install a module from npm and add it to the modules array in nuxt.config.ts.

# Install a module
npx nuxi module add @nuxt/content

# Or manually install and configure
npm install @nuxt/image @pinia/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    "@nuxt/content",
    "@nuxt/image",
    "@pinia/nuxt",
    "@nuxtjs/i18n",
  ],

  // Module-specific configuration
  content: {
    highlight: {
      theme: "github-dark",
    },
  },

  image: {
    quality: 80,
    formats: ["avif", "webp"],
  },

  i18n: {
    locales: ["en", "fr", "de"],
    defaultLocale: "en",
  },
})

Route Middleware

Middleware are navigation guards that run before rendering a page. Nuxt supports three types: anonymous (inline), named (in middleware/ directory), and global (with .global suffix).

Named Middleware

Create middleware files in the middleware/ directory and reference them in page components with definePageMeta.

// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const { isLoggedIn } = useAuth()
  if (!isLoggedIn.value) return navigateTo("/login")
})

// middleware/role.ts
export default defineNuxtRouteMiddleware((to) => {
  const { user } = useAuth()
  if (to.meta.role && user.value?.role !== to.meta.role) {
    return navigateTo("/unauthorized")
  }
})
<!-- pages/dashboard.vue -->
<script setup lang="ts">
definePageMeta({ middleware: ["auth"] })
</script>

<!-- pages/admin.vue - multiple middleware -->
<script setup lang="ts">
definePageMeta({ middleware: ["auth", "role"], role: "admin" })
</script>

Global Middleware

Files with the .global suffix in middleware/ run on every route navigation automatically.

// middleware/analytics.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
  // Runs on every route change
  if (import.meta.client) {
    trackPageView(to.fullPath)
  }
})

Layouts

Layouts are wrapper components that surround pages. They are useful for shared UI elements like headers, sidebars, and footers. Define layouts in the layouts/ directory.

<!-- layouts/default.vue -->
<template>
  <div>
    <AppHeader />
    <main>
      <slot />
    </main>
    <AppFooter />
  </div>
</template>

Custom Layouts

Create multiple layouts for different sections of your application. Pages opt into a layout using definePageMeta.

<!-- layouts/admin.vue -->
<template>
  <div style="display: flex">
    <AdminSidebar />
    <main style="flex: 1; padding: 2rem">
      <slot />
    </main>
  </div>
</template>

<!-- pages/admin/index.vue -->
<script setup lang="ts">
definePageMeta({
  layout: "admin",
})
</script>

<template>
  <div>
    <h1>Admin Dashboard</h1>
  </div>
</template>

Plugins

Plugins run on application initialization and have access to the Nuxt app instance. Use them to register global components, directives, or third-party libraries.

Creating Plugins

Create files in the plugins/ directory. They are auto-registered and run before rendering the root component.

// plugins/api.ts
export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig()
  const api = $fetch.create({
    baseURL: config.public.apiBase,
    onRequest({ options }) {
      const token = useCookie("auth-token")
      if (token.value) {
        options.headers.set("Authorization", `Bearer \${token.value}`)
      }
    },
    onResponseError({ response }) {
      if (response.status === 401) navigateTo("/login")
    },
  })
  return { provide: { api } }
})

// Usage: const { $api } = useNuxtApp()
// const data = await $api("/users")

Rendering Modes

Nuxt 3 supports multiple rendering strategies that can be configured globally or per route.

Server-Side Rendering (SSR)

The default mode. Pages are rendered on the server for each request, providing fast first contentful paint and SEO benefits.

// nuxt.config.ts - SSR is enabled by default
export default defineNuxtConfig({
  ssr: true, // default
})

Static Site Generation (SSG)

Pre-render all pages at build time. Ideal for content sites, documentation, and marketing pages.

# Pre-render all pages as static HTML
npx nuxi generate

# Output is in .output/public/ - deploy to any CDN

Hybrid Rendering

Configure different rendering strategies per route using route rules in nuxt.config.ts.

// nuxt.config.ts - hybrid rendering
export default defineNuxtConfig({
  routeRules: {
    "/": { prerender: true },           // Static at build
    "/about": { prerender: true },
    "/blog/**": { isr: 60 },            // ISR: regen every 60s
    "/admin/**": { ssr: false },         // SPA: client-only
    "/api/**": { cache: { maxAge: 300 }, cors: true },
    "/old-page": { redirect: "/new-page" },
  },
})

Deployment

Nuxt 3 deploys to many platforms with zero-config presets. Nitro detects the deployment platform and optimizes the output automatically.

Deploying to Vercel

Vercel has first-class Nuxt support. Push to Git and Vercel auto-detects and deploys.

# Vercel auto-detects Nuxt - just push to Git
npx vercel

# Or set preset explicitly in nuxt.config.ts:
# nitro: { preset: "vercel" }

Deploying to Cloudflare Workers

Nuxt runs on Cloudflare Workers at the edge for ultra-low latency globally.

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: "cloudflare-pages",
  },
})

// Deploy with Wrangler
// npx wrangler pages deploy .output/public

Node.js Server

Deploy to any Node.js server with PM2, Docker, or a cloud VM.

# Build and start
npm run build
node .output/server/index.mjs

# PM2 process management
pm2 start .output/server/index.mjs --name my-nuxt-app

# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY .output .output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

Nuxt vs Next.js

Nuxt and Next.js are the leading meta-frameworks for Vue and React respectively. Here is how they compare.

FeatureNuxt 3Next.js 15
Base FrameworkVue 3React 19
Build ToolViteTurbopack / webpack
Server EngineNitro (H3)Built-in (Node.js)
Auto-ImportsBuilt-in (composables, components, Vue APIs)Not built-in
File Routingpages/ directoryapp/ directory
Data FetchinguseFetch, useAsyncDataServer Components, fetch()
State ManagementuseState (built-in) + PiniaReact Context + Zustand/Jotai
Rendering ModesSSR, SSG, ISR, SPA, Hybrid per-routeSSR, SSG, ISR, SPA, Streaming
Edge DeploymentCloudflare Workers, Deno, BunVercel Edge, Cloudflare
TypeScriptBuilt-in, auto-generated typesBuilt-in
Module Ecosystem200+ modulesnpm packages (no module system)
Learning CurveLower (Vue is simpler)Moderate (React + RSC complexity)

Frequently Asked Questions

What is the difference between Nuxt 2 and Nuxt 3?

Nuxt 3 is a complete rewrite built on Vue 3, Vite, and Nitro. It offers significantly better performance, TypeScript support, auto-imports, the Composition API, hybrid rendering, and a new server engine. Nuxt 2 was built on Vue 2 and webpack.

Is Nuxt 3 ready for production?

Yes. Nuxt 3 reached stable release in November 2022 and is actively maintained. Major companies use Nuxt 3 in production. The ecosystem has matured with most essential modules ported to Nuxt 3.

Should I use Nuxt or plain Vue for my project?

Use Nuxt if you need SEO, server-side rendering, file-based routing, or a full-stack framework. Use plain Vue with Vite for client-side SPAs, widgets, or when you want full control over the build configuration.

How does useFetch differ from useAsyncData?

useFetch is a convenience wrapper around useAsyncData that automatically handles URL-based data fetching with caching. useAsyncData is more flexible and works with any async function, not just HTTP requests. Use useFetch for API calls and useAsyncData for custom async operations.

Can I use Nuxt for static sites?

Yes. Run npx nuxi generate to pre-render all pages as static HTML files. Nuxt also supports hybrid rendering where some routes are static and others are server-rendered.

How does auto-import work in Nuxt 3?

Nuxt scans the composables/, components/, and utils/ directories and automatically imports their exports. Vue APIs like ref, computed, and watch are also auto-imported. This is done at build time via code transformation, so there is no runtime overhead.

What is Nitro and why does it matter?

Nitro is the server engine powering Nuxt 3. It provides a universal runtime that can deploy to Node.js, Cloudflare Workers, Deno, Netlify Functions, and more. Nitro handles server routes, caching, storage, and output optimization for each deployment target.

How do I add authentication to a Nuxt app?

Use the nuxt-auth-utils module for session-based auth, or sidebase-auth for OAuth providers. You can also implement custom auth using server middleware, useState for session state, and server routes for login and registration endpoints.

𝕏 Twitterin LinkedIn
Apakah ini membantu?

Tetap Update

Dapatkan tips dev mingguan dan tool baru.

Tanpa spam. Berhenti kapan saja.

Coba Alat Terkait

{ }JSON FormatterJSON Validator

Artikel Terkait

Gatsby.js: The Complete Guide to the React Static Site Generator

Master Gatsby.js with GraphQL data layer, plugins, image optimization, SSR/DSG, headless CMS integration, and deployment strategies.

Deno: The Complete Guide to the Secure JavaScript Runtime

Master Deno runtime with security permissions, TypeScript support, standard library, HTTP server, testing, npm compatibility, and Deno Deploy.

SWC: The Complete Guide to the Speedy Web Compiler

Master SWC for ultra-fast JavaScript/TypeScript compilation with configuration, transformations, minification, and framework integration.