
Nuxt
- 20.3k installs
- 5.7k repo stars
- Updated June 23, 2026
- antfu/skills
Nuxt is a full-stack Vue framework that provides server-side rendering, file-based routing, and universal deployment.
About
Nuxt is a full-stack Vue framework that provides universal deployment capabilities through Nitro, file-based routing, and auto-imports. It supports server-side rendering, client-side rendering, and hybrid rendering modes for flexible application architecture. Developers use it to build production-grade web applications with SSR and edge deployment support.
- Full-stack Vue framework with server-side rendering and file-based routing
- Nitro server engine for deployment across Node.js, serverless, and edge platforms
- Auto-import system for components and composables with Nuxt 4 app/ directory structure
Nuxt by the numbers
- 20,280 all-time installs (skills.sh)
- +426 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #29 of 2,244 Frontend Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
nuxt capabilities & compatibility
- Works with
- vercel · cloudflare
npx skills add https://github.com/antfu/skills --skill nuxtAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 20.3k |
|---|---|
| repo stars | ★ 5.7k |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 23, 2026 |
| Repository | antfu/skills ↗ |
How do you use Nuxt and Nitro lifecycle hooks?
Build full-stack Vue applications with server-side rendering and universal deployment.
Who is it for?
Nuxt developers extending framework build pipelines, custom page registration, or Nitro server runtime without modifying core Nuxt source.
Skip if: Developers on plain Vue or Next.js projects, or teams needing only component styling guidance without framework hook APIs.
When should I use this skill?
A Nuxt or Nitro project needs custom build:before logic, pages:extend routes, or server runtime hook handlers in configuration.
What you get
Hook handlers wired in nuxt.config.ts or modules for build-time and runtime extension points
- Hook handlers in nuxt.config.ts or modules
- Custom page routes via pages:extend
By the numbers
- Generated 2026-01-28 from sources/nuxt
- Source git SHA c9fed804b9bef362276033b03ca43730c6efa7dc
Files
Nuxt is a full-stack Vue framework that provides server-side rendering, file-based routing, auto-imports, and a powerful module system. It uses Nitro as its server engine for universal deployment across Node.js, serverless, and edge platforms.
The skill is based on Nuxt 3.x, generated at 2026-01-28.
Core
| Topic | Description | Reference |
|---|---|---|
| Directory Structure | Project folder structure, conventions, file organization | core-directory-structure |
| Configuration | nuxt.config.ts, app.config.ts, runtime config, environment variables | core-config |
| CLI Commands | Dev server, build, generate, preview, and utility commands | core-cli |
| Routing | File-based routing, dynamic routes, navigation, middleware, layouts | core-routing |
| Data Fetching | useFetch, useAsyncData, $fetch, caching, refresh | core-data-fetching |
| Modules | Creating and using Nuxt modules, Nuxt Kit utilities | core-modules |
| Deployment | Platform-agnostic deployment with Nitro, Vercel, Netlify, Cloudflare | core-deployment |
Features
| Topic | Description | Reference |
|---|---|---|
| Composables Auto-imports | Vue APIs, Nuxt composables, custom composables, utilities | features-composables |
| Components Auto-imports | Component naming, lazy loading, hydration strategies | features-components-autoimport |
| Built-in Components | NuxtLink, NuxtPage, NuxtLayout, ClientOnly, and more | features-components |
| State Management | useState composable, SSR-friendly state, Pinia integration | features-state |
| Server Routes | API routes, server middleware, Nitro server engine | features-server |
Rendering
| Topic | Description | Reference |
|---|---|---|
| Rendering Modes | Universal (SSR), client-side (SPA), hybrid rendering, route rules | rendering-modes |
Best Practices
| Topic | Description | Reference |
|---|---|---|
| Data Fetching Patterns | Efficient fetching, caching, parallel requests, error handling | best-practices-data-fetching |
| SSR & Hydration | Avoiding context leaks, hydration mismatches, composable patterns | best-practices-ssr |
Advanced
| Topic | Description | Reference |
|---|---|---|
| Layers | Extending applications with reusable layers | advanced-layers |
| Lifecycle Hooks | Build-time, runtime, and server hooks | advanced-hooks |
| Module Authoring | Creating publishable Nuxt modules with Nuxt Kit | advanced-module-authoring |
Generation Info
- Source:
sources/nuxt - Git SHA:
c9fed804b9bef362276033b03ca43730c6efa7dc - Generated: 2026-01-28
Lifecycle Hooks
Nuxt provides hooks to tap into the build process, application lifecycle, and server runtime.
Build-time Hooks (Nuxt)
Used in nuxt.config.ts or modules:
In nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
hooks: {
'build:before': () => {
console.log('Build starting...')
},
'pages:extend': (pages) => {
// Add custom pages
pages.push({
name: 'custom',
path: '/custom',
file: '~/pages/custom.vue',
})
},
'components:dirs': (dirs) => {
// Add component directories
dirs.push({ path: '~/extra-components' })
},
},
})In Modules
// modules/my-module.ts
export default defineNuxtModule({
setup(options, nuxt) {
nuxt.hook('ready', async (nuxt) => {
console.log('Nuxt is ready')
})
nuxt.hook('close', async (nuxt) => {
console.log('Nuxt is closing')
})
nuxt.hook('modules:done', () => {
console.log('All modules loaded')
})
},
})Common Build Hooks
| Hook | When |
|---|---|
ready | Nuxt initialization complete |
close | Nuxt is closing |
modules:done | All modules installed |
build:before | Before build starts |
build:done | Build complete |
pages:extend | Pages routes resolved |
components:dirs | Component dirs being resolved |
imports:extend | Auto-imports being resolved |
nitro:config | Before Nitro config finalized |
vite:extend | Vite context created |
vite:extendConfig | Before Vite config finalized |
App Hooks (Runtime)
Used in plugins and composables:
In Plugins
// plugins/lifecycle.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('app:created', (vueApp) => {
console.log('Vue app created')
})
nuxtApp.hook('app:mounted', (vueApp) => {
console.log('App mounted')
})
nuxtApp.hook('page:start', () => {
console.log('Page navigation starting')
})
nuxtApp.hook('page:finish', () => {
console.log('Page navigation finished')
})
nuxtApp.hook('page:loading:start', () => {
console.log('Page loading started')
})
nuxtApp.hook('page:loading:end', () => {
console.log('Page loading ended')
})
})Common App Hooks
| Hook | When |
|---|---|
app:created | Vue app created |
app:mounted | Vue app mounted (client only) |
app:error | Fatal error occurred |
page:start | Page navigation starting |
page:finish | Page navigation finished |
page:loading:start | Loading indicator should show |
page:loading:end | Loading indicator should hide |
link:prefetch | Link is being prefetched |
Using Runtime Hooks
// composables/usePageTracking.ts
export function usePageTracking() {
const nuxtApp = useNuxtApp()
nuxtApp.hook('page:finish', () => {
trackPageView(useRoute().path)
})
}Server Hooks (Nitro)
Used in server plugins:
// server/plugins/hooks.ts
export default defineNitroPlugin((nitroApp) => {
// Modify HTML before sending
nitroApp.hooks.hook('render:html', (html, { event }) => {
html.head.push('<meta name="custom" content="value">')
html.bodyAppend.push('<script>console.log("injected")</script>')
})
// Modify response
nitroApp.hooks.hook('render:response', (response, { event }) => {
console.log('Sending response:', response.statusCode)
})
// Before request
nitroApp.hooks.hook('request', (event) => {
console.log('Request:', event.path)
})
// After response
nitroApp.hooks.hook('afterResponse', (event) => {
console.log('Response sent')
})
})Common Nitro Hooks
| Hook | When |
|---|---|
request | Request received |
beforeResponse | Before sending response |
afterResponse | After response sent |
render:html | Before HTML is sent |
render:response | Before response is finalized |
error | Error occurred |
Custom Hooks
Define Custom Hook Types
// types/hooks.d.ts
import type { HookResult } from '@nuxt/schema'
declare module '#app' {
interface RuntimeNuxtHooks {
'my-app:event': (data: MyEventData) => HookResult
}
}
declare module '@nuxt/schema' {
interface NuxtHooks {
'my-module:init': () => HookResult
}
}
declare module 'nitropack/types' {
interface NitroRuntimeHooks {
'my-server:event': (data: any) => void
}
}Call Custom Hooks
// In a plugin
export default defineNuxtPlugin((nuxtApp) => {
// Call custom hook
nuxtApp.callHook('my-app:event', { type: 'custom' })
})
// In a module
export default defineNuxtModule({
setup(options, nuxt) {
nuxt.callHook('my-module:init')
},
})useRuntimeHook
Call hooks at runtime from components:
<script setup lang="ts">
// Register a callback for a runtime hook
useRuntimeHook('app:error', (error) => {
console.error('App error:', error)
})
</script>Hook Examples
Page View Tracking
// plugins/analytics.client.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('page:finish', () => {
const route = useRoute()
analytics.track('pageview', {
path: route.path,
title: document.title,
})
})
})Performance Monitoring
// plugins/performance.client.ts
export default defineNuxtPlugin((nuxtApp) => {
let navigationStart: number
nuxtApp.hook('page:start', () => {
navigationStart = performance.now()
})
nuxtApp.hook('page:finish', () => {
const duration = performance.now() - navigationStart
console.log(`Navigation took ${duration}ms`)
})
})Inject HTML
// server/plugins/inject.ts
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:html', (html) => {
html.head.push(`
<script>
window.APP_CONFIG = ${JSON.stringify(config)}
</script>
`)
})
})<!-- Source references:
- https://nuxt.com/docs/guide/going-further/hooks
- https://nuxt.com/docs/api/advanced/hooks
-->
Nuxt Layers
Layers allow sharing and reusing partial Nuxt applications across projects. They can include components, composables, pages, layouts, and configuration.
Using Layers
From npm Package
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
'@my-org/base-layer',
'@nuxtjs/ui-layer',
],
})From Git Repository
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
'github:username/repo',
'github:username/repo/base', // Subdirectory
'github:username/repo#v1.0', // Specific tag
'github:username/repo#dev', // Branch
'gitlab:username/repo',
'bitbucket:username/repo',
],
})From Local Directory
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
'../base-layer',
'./layers/shared',
],
})Auto-scanned Layers
Place in layers/ directory for automatic discovery:
my-app/
├── layers/
│ ├── base/
│ │ └── nuxt.config.ts
│ └── ui/
│ └── nuxt.config.ts
└── nuxt.config.tsCreating a Layer
Minimal layer structure:
my-layer/
├── nuxt.config.ts # Required
├── app/
│ ├── components/ # Auto-merged
│ ├── composables/ # Auto-merged
│ ├── layouts/ # Auto-merged
│ ├── middleware/ # Auto-merged
│ ├── pages/ # Auto-merged
│ ├── plugins/ # Auto-merged
│ └── app.config.ts # Merged
├── server/ # Auto-merged
└── package.jsonLayer nuxt.config.ts
// my-layer/nuxt.config.ts
export default defineNuxtConfig({
// Layer configuration
app: {
head: {
title: 'My Layer App',
},
},
// Shared modules
modules: ['@nuxt/ui'],
})Layer Components
<!-- my-layer/app/components/BaseButton.vue -->
<template>
<button class="base-btn">
<slot />
</button>
</template>Use in consuming project:
<template>
<BaseButton>Click me</BaseButton>
</template>Layer Composables
// my-layer/app/composables/useTheme.ts
export function useTheme() {
const isDark = useState('theme-dark', () => false)
const toggle = () => isDark.value = !isDark.value
return { isDark, toggle }
}Layer Priority
Override order (highest to lowest): 1. Your project files 2. Auto-scanned layers (alphabetically, Z > A) 3. extends array (first > last)
Control order with prefixes:
layers/
├── 1.base/ # Lower priority
└── 2.theme/ # Higher priorityLayer Aliases
Access layer files:
// Auto-scanned layers get aliases
import Component from '#layers/base/components/Component.vue'Named aliases:
// my-layer/nuxt.config.ts
export default defineNuxtConfig({
$meta: {
name: 'my-layer',
},
})// In consuming project
import { something } from '#layers/my-layer/utils'Publishing Layers
As npm Package
{
"name": "my-nuxt-layer",
"version": "1.0.0",
"type": "module",
"main": "./nuxt.config.ts",
"dependencies": {
"@nuxt/ui": "^2.0.0"
},
"devDependencies": {
"nuxt": "^3.0.0"
}
}Private Layers
For private git repos:
export GIGET_AUTH=<github-token>Layer Best Practices
Use Resolved Paths
// my-layer/nuxt.config.ts
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
const currentDir = dirname(fileURLToPath(import.meta.url))
export default defineNuxtConfig({
css: [
join(currentDir, './assets/main.css'),
],
})Install Dependencies
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
['github:user/layer', { install: true }],
],
})Disable Layer Modules
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['./base-layer'],
// Disable modules from layer
image: false, // Disables @nuxt/image
pinia: false, // Disables @pinia/nuxt
})Starter Template
Create a new layer:
npx nuxi init --template layer my-layerExample: Theme Layer
theme-layer/
├── nuxt.config.ts
├── app/
│ ├── app.config.ts
│ ├── components/
│ │ ├── ThemeButton.vue
│ │ └── ThemeCard.vue
│ ├── composables/
│ │ └── useTheme.ts
│ └── assets/
│ └── theme.css
└── package.json// theme-layer/nuxt.config.ts
export default defineNuxtConfig({
css: ['~/assets/theme.css'],
})// theme-layer/app/app.config.ts
export default defineAppConfig({
theme: {
primaryColor: '#00dc82',
darkMode: false,
},
})// consuming-app/nuxt.config.ts
export default defineNuxtConfig({
extends: ['theme-layer'],
})
// consuming-app/app/app.config.ts
export default defineAppConfig({
theme: {
primaryColor: '#ff0000', // Override
},
})<!-- Source references:
- https://nuxt.com/docs/getting-started/layers
- https://nuxt.com/docs/guide/going-further/layers
-->
Module Authoring
This guide covers creating publishable Nuxt modules with proper structure, type safety, and best practices.
Module Structure
Recommended structure for a publishable module:
my-nuxt-module/
├── src/
│ ├── module.ts # Module entry
│ └── runtime/
│ ├── components/ # Vue components
│ ├── composables/ # Composables
│ ├── plugins/ # Nuxt plugins
│ └── server/ # Server handlers
├── playground/ # Development app
├── package.json
└── tsconfig.jsonModule Definition
Basic Module with Type-safe Options
// src/module.ts
import { defineNuxtModule, createResolver, addPlugin, addComponent, addImports } from '@nuxt/kit'
export interface ModuleOptions {
prefix?: string
apiKey: string
enabled?: boolean
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'my-module',
configKey: 'myModule',
compatibility: {
nuxt: '>=3.0.0',
},
},
defaults: {
prefix: 'My',
enabled: true,
},
setup(options, nuxt) {
if (!options.enabled) return
const { resolve } = createResolver(import.meta.url)
// Module setup logic here
},
})Using .with() for Strict Type Inference
When you need TypeScript to infer that default values are always present:
import { defineNuxtModule } from '@nuxt/kit'
interface ModuleOptions {
apiKey: string
baseURL: string
timeout?: number
}
export default defineNuxtModule<ModuleOptions>().with({
meta: {
name: '@nuxtjs/my-api',
configKey: 'myApi',
},
defaults: {
baseURL: 'https://api.example.com',
timeout: 5000,
},
setup(resolvedOptions, nuxt) {
// resolvedOptions.baseURL is guaranteed to be string (not undefined)
// resolvedOptions.timeout is guaranteed to be number (not undefined)
},
})Adding Runtime Assets
Components
import { addComponent, addComponentsDir, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
// Single component
addComponent({
name: 'MyButton',
filePath: resolve('./runtime/components/MyButton.vue'),
})
// Component directory with prefix
addComponentsDir({
path: resolve('./runtime/components'),
prefix: 'My',
pathPrefix: false,
})
},
})Composables and Auto-imports
import { addImports, addImportsDir, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
// Single import
addImports({
name: 'useMyUtil',
from: resolve('./runtime/composables/useMyUtil'),
})
// Directory of composables
addImportsDir(resolve('./runtime/composables'))
},
})Plugins
import { addPlugin, addPluginTemplate, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup(options) {
const { resolve } = createResolver(import.meta.url)
// Static plugin file
addPlugin({
src: resolve('./runtime/plugins/myPlugin'),
mode: 'client', // 'client', 'server', or 'all'
})
// Dynamic plugin with generated code
addPluginTemplate({
filename: 'my-module-plugin.mjs',
getContents: () => `
import { defineNuxtPlugin } from '#app/nuxt'
export default defineNuxtPlugin({
name: 'my-module',
setup() {
const config = ${JSON.stringify(options)}
// Plugin logic
}
})`,
})
},
})Server Extensions
Server Handlers
import { addServerHandler, addServerScanDir, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
// Single handler
addServerHandler({
route: '/api/my-endpoint',
handler: resolve('./runtime/server/api/my-endpoint'),
})
// Scan entire server directory (api/, routes/, middleware/, utils/)
addServerScanDir(resolve('./runtime/server'))
},
})Server Composables
import { addServerImports, addServerImportsDir, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
// Single server import
addServerImports({
name: 'useServerUtil',
from: resolve('./runtime/server/utils/useServerUtil'),
})
// Server composables directory
addServerImportsDir(resolve('./runtime/server/composables'))
},
})Nitro Plugin
import { addServerPlugin, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
addServerPlugin(resolve('./runtime/server/plugin'))
},
})// runtime/server/plugin.ts
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('request', (event) => {
console.log('Request:', event.path)
})
})Templates and Virtual Files
Generate Virtual Files
import { addTemplate, addTypeTemplate, addServerTemplate, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
// Client/build virtual file (accessible via #build/my-config.mjs)
addTemplate({
filename: 'my-config.mjs',
getContents: () => `export default ${JSON.stringify(options)}`,
})
// Type declarations
addTypeTemplate({
filename: 'types/my-module.d.ts',
getContents: () => `
declare module '#my-module' {
export interface Config {
apiKey: string
}
}`,
})
// Nitro virtual file (accessible in server routes)
addServerTemplate({
filename: '#my-module/config.mjs',
getContents: () => `export const config = ${JSON.stringify(options)}`,
})
},
})Access Virtual Files
// In runtime plugin
// @ts-expect-error - virtual file
import config from '#build/my-config.mjs'
// In server routes
import { config } from '#my-module/config.js'Extending Pages and Routes
import { extendPages, extendRouteRules, addRouteMiddleware, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
// Add pages
extendPages((pages) => {
pages.push({
name: 'my-page',
path: '/my-route',
file: resolve('./runtime/pages/MyPage.vue'),
})
})
// Add route rules (caching, redirects, etc.)
extendRouteRules('/api/**', {
cache: { maxAge: 60 },
})
// Add middleware
addRouteMiddleware({
name: 'my-middleware',
path: resolve('./runtime/middleware/myMiddleware'),
global: true,
})
},
})Module Dependencies
Declare dependencies on other modules with version constraints:
export default defineNuxtModule({
meta: {
name: 'my-module',
},
moduleDependencies: {
'@nuxtjs/tailwindcss': {
version: '>=6.0.0',
// Set defaults (user can override)
defaults: {
exposeConfig: true,
},
// Force specific options
overrides: {
viewer: false,
},
},
'@nuxtjs/i18n': {
optional: true, // Won't fail if not installed
defaults: {
defaultLocale: 'en',
},
},
},
setup() {
// Dependencies are guaranteed to be set up before this runs
},
})Dynamic Dependencies
moduleDependencies(nuxt) {
const deps: Record<string, any> = {
'@nuxtjs/tailwindcss': { version: '>=6.0.0' },
}
if (nuxt.options.ssr) {
deps['@nuxtjs/html-validator'] = { optional: true }
}
return deps
}Lifecycle Hooks
Requires meta.name and meta.version:
export default defineNuxtModule({
meta: {
name: 'my-module',
version: '1.2.0',
},
onInstall(nuxt) {
// First-time setup
console.log('Module installed for the first time')
},
onUpgrade(nuxt, options, previousVersion) {
// Version upgrade migrations
console.log(`Upgrading from ${previousVersion}`)
},
setup(options, nuxt) {
// Regular setup runs every build
},
})Extending Configuration
export default defineNuxtModule({
setup(options, nuxt) {
// Add CSS
nuxt.options.css.push('my-module/styles.css')
// Add runtime config
nuxt.options.runtimeConfig.public.myModule = {
apiUrl: options.apiUrl,
}
// Extend Vite config
nuxt.options.vite.optimizeDeps ||= {}
nuxt.options.vite.optimizeDeps.include ||= []
nuxt.options.vite.optimizeDeps.include.push('some-package')
// Add build transpile
nuxt.options.build.transpile.push('my-package')
},
})Using Hooks
export default defineNuxtModule({
// Declarative hooks
hooks: {
'components:dirs': (dirs) => {
dirs.push({ path: '~/extra' })
},
},
setup(options, nuxt) {
// Programmatic hooks
nuxt.hook('pages:extend', (pages) => {
// Modify pages
})
nuxt.hook('imports:extend', (imports) => {
imports.push({ name: 'myHelper', from: 'my-package' })
})
nuxt.hook('nitro:config', (config) => {
// Modify Nitro config
})
nuxt.hook('vite:extendConfig', (config) => {
// Modify Vite config
})
},
})Path Resolution
import { createResolver, resolvePath, findPath } from '@nuxt/kit'
export default defineNuxtModule({
async setup(options, nuxt) {
// Resolver relative to module
const { resolve } = createResolver(import.meta.url)
const pluginPath = resolve('./runtime/plugin')
// Resolve with extensions and aliases
const entrypoint = await resolvePath('@some/package')
// Find first existing file
const configPath = await findPath([
resolve('./config.ts'),
resolve('./config.js'),
])
},
})Module Package.json
{
"name": "my-nuxt-module",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": "./dist/module.mjs",
"require": "./dist/module.cjs"
}
},
"main": "./dist/module.cjs",
"module": "./dist/module.mjs",
"types": "./dist/types.d.ts",
"files": ["dist"],
"scripts": {
"dev": "nuxi dev playground",
"build": "nuxt-module-build build",
"prepare": "nuxt-module-build build --stub"
},
"dependencies": {
"@nuxt/kit": "^3.0.0"
},
"devDependencies": {
"@nuxt/module-builder": "latest",
"nuxt": "^3.0.0"
}
}Disabling Modules
Users can disable a module via config key:
// nuxt.config.ts
export default defineNuxtConfig({
// Disable entirely
myModule: false,
// Or with options
myModule: {
enabled: false,
},
})Development Workflow
1. Create module: npx nuxi init -t module my-module 2. Develop: npm run dev (runs playground) 3. Build: npm run build 4. Test: npm run test
Best Practices
- Use
createResolver(import.meta.url)for all path resolution - Prefix components to avoid naming conflicts
- Make options type-safe with
ModuleOptionsinterface - Use
moduleDependenciesinstead ofinstallModule - Provide sensible defaults for all options
- Add compatibility requirements in
meta.compatibility - Use virtual files for dynamic configuration
- Separate client/server plugins appropriately
<!-- Source references:
- https://nuxt.com/docs/api/kit/modules
- https://nuxt.com/docs/api/kit/components
- https://nuxt.com/docs/api/kit/autoimports
- https://nuxt.com/docs/api/kit/plugins
- https://nuxt.com/docs/api/kit/templates
- https://nuxt.com/docs/api/kit/nitro
- https://nuxt.com/docs/api/kit/pages
- https://nuxt.com/docs/api/kit/resolving
-->
Data Fetching Best Practices
Effective data fetching patterns for SSR-friendly, performant Nuxt applications.
Choose the Right Tool
| Scenario | Use |
|---|---|
| Component initial data | useFetch or useAsyncData |
| User interactions (clicks, forms) | $fetch |
| Third-party SDK/API | useAsyncData with custom function |
| Multiple parallel requests | useAsyncData with Promise.all |
Await vs Non-Await Usage
The await keyword controls whether data fetching blocks navigation:
With await - Blocking Navigation
<script setup lang="ts">
// Navigation waits until data is fetched (uses Vue Suspense)
const { data } = await useFetch('/api/posts')
// data.value is available immediately after this line
</script>- Server: Fetches data and includes it in the payload
- Client hydration: Uses payload data, no re-fetch
- Client navigation: Blocks until data is ready
Without await - Non-Blocking (Lazy)
<script setup lang="ts">
// Navigation proceeds immediately, data fetches in background
const { data, status } = useFetch('/api/posts', { lazy: true })
// data.value may be undefined initially - check status!
</script>
<template>
<div v-if="status === 'pending'">Loading...</div>
<div v-else>{{ data }}</div>
</template>Equivalent to using useLazyFetch:
<script setup lang="ts">
const { data, status } = useLazyFetch('/api/posts')
</script>When to Use Each
| Pattern | Use Case |
|---|---|
await useFetch() | Critical data needed for SEO/initial render |
useFetch({ lazy: true }) | Non-critical data, better perceived performance |
await useLazyFetch() | Same as lazy, await only ensures initialization |
Avoid Double Fetching
❌ Wrong: Using $fetch Alone in Setup
<script setup lang="ts">
// This fetches TWICE: once on server, once on client
const data = await $fetch('/api/posts')
</script>✅ Correct: Use useFetch
<script setup lang="ts">
// Fetches on server, hydrates on client (no double fetch)
const { data } = await useFetch('/api/posts')
</script>Use Explicit Cache Keys
❌ Avoid: Auto-generated Keys
<script setup lang="ts">
// Key is auto-generated from file/line - can cause issues
const { data } = await useAsyncData(() => fetchPosts())
</script>✅ Better: Explicit Keys
<script setup lang="ts">
// Explicit key for predictable caching
const { data } = await useAsyncData(
'posts',
() => fetchPosts(),
)
// Dynamic keys for parameterized data
const route = useRoute()
const { data: post } = await useAsyncData(
`post-${route.params.id}`,
() => fetchPost(route.params.id),
)
</script>Handle Loading States Properly
<script setup lang="ts">
const { data, status, error } = await useFetch('/api/posts')
</script>
<template>
<div v-if="status === 'pending'">
<SkeletonLoader />
</div>
<div v-else-if="error">
<ErrorMessage :error="error" />
</div>
<div v-else>
<PostList :posts="data" />
</div>
</template>Use Lazy Fetching for Non-critical Data
<script setup lang="ts">
const id = useRoute().params.id
// Critical data - blocks navigation
const { data: post } = await useFetch(`/api/posts/${id}`)
// Non-critical data - doesn't block navigation
const { data: comments, status } = useFetch(`/api/posts/${id}/comments`, {
lazy: true,
})
// Or use useLazyFetch
const { data: related } = useLazyFetch(`/api/posts/${id}/related`)
</script>
<template>
<article>
<h1>{{ post?.title }}</h1>
<p>{{ post?.content }}</p>
</article>
<section v-if="status === 'pending'">Loading comments...</section>
<CommentList v-else :comments="comments" />
</template>Minimize Payload Size
Use pick for Simple Filtering
<script setup lang="ts">
const { data } = await useFetch('/api/users', {
// Only include these fields in payload
pick: ['id', 'name', 'avatar'],
})
</script>Use transform for Complex Transformations
<script setup lang="ts">
const { data } = await useFetch('/api/posts', {
transform: (posts) => {
return posts.map(post => ({
id: post.id,
title: post.title,
excerpt: post.content.slice(0, 100),
date: new Date(post.createdAt).toLocaleDateString(),
}))
},
})
</script>Parallel Fetching
Fetch Independent Data with useAsyncData
<script setup lang="ts">
const { data } = await useAsyncData(
'dashboard',
async (_nuxtApp, { signal }) => {
const [user, posts, stats] = await Promise.all([
$fetch('/api/user', { signal }),
$fetch('/api/posts', { signal }),
$fetch('/api/stats', { signal }),
])
return { user, posts, stats }
},
)
</script>Multiple useFetch Calls
<script setup lang="ts">
// These run in parallel automatically
const [{ data: user }, { data: posts }] = await Promise.all([
useFetch('/api/user'),
useFetch('/api/posts'),
])
</script>Efficient Refresh Patterns
Watch Reactive Dependencies
<script setup lang="ts">
const page = ref(1)
const category = ref('all')
const { data } = await useFetch('/api/posts', {
query: { page, category },
// Auto-refresh when these change
watch: [page, category],
})
</script>Manual Refresh
<script setup lang="ts">
const { data, refresh, status } = await useFetch('/api/posts')
async function refreshPosts() {
await refresh()
}
</script>Conditional Fetching
<script setup lang="ts">
const userId = ref<string | null>(null)
const { data, execute } = useFetch(() => `/api/users/${userId.value}`, {
immediate: false, // Don't fetch until userId is set
})
// Later, when userId is available
function loadUser(id: string) {
userId.value = id
execute()
}
</script>Server-only Fetching
<script setup lang="ts">
// Only fetch on server, skip on client navigation
const { data } = await useFetch('/api/static-content', {
server: true,
lazy: true,
getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key],
})
</script>Error Handling
<script setup lang="ts">
const { data, error, refresh } = await useFetch('/api/posts')
// Watch for errors if need event-like handling
watch(error, (err) => {
if (err) {
console.error('Fetch failed:', err)
// Show toast, redirect, etc.
}
}, { immediate: true })
</script>
<template>
<div v-if="error">
<p>Failed to load: {{ error.message }}</p>
<button @click="refresh()">Retry</button>
</div>
</template>Shared Data Across Components
<!-- ComponentA.vue -->
<script setup lang="ts">
const { data } = await useFetch('/api/user', { key: 'current-user' })
</script>
<!-- ComponentB.vue -->
<script setup lang="ts">
// Access cached data without refetching
const { data: user } = useNuxtData('current-user')
// Or refresh it
const { refresh } = await useFetch('/api/user', { key: 'current-user' })
</script>Avoid useAsyncData for Side Effects
❌ Wrong: Side Effects in useAsyncData
<script setup lang="ts">
// Don't trigger Pinia actions or side effects
await useAsyncData(() => store.fetchUser()) // Can cause issues
</script>✅ Correct: Use callOnce for Side Effects
<script setup lang="ts">
await callOnce(async () => {
await store.fetchUser()
})
</script><!-- Source references:
- https://nuxt.com/docs/getting-started/data-fetching
- https://nuxt.com/docs/api/composables/use-fetch
- https://nuxt.com/docs/api/composables/use-async-data
- https://nuxt.com/docs/api/composables/use-lazy-fetch
-->
SSR Best Practices
Patterns for avoiding common SSR pitfalls: context leaks, hydration mismatches, and composable errors.
The "Nuxt Instance Unavailable" Error
This error occurs when calling Nuxt composables outside the proper context.
❌ Wrong: Composable Outside Setup
// composables/bad.ts
// Called at module level - no Nuxt context!
const config = useRuntimeConfig()
export function useMyComposable() {
return config.public.apiBase
}✅ Correct: Composable Inside Function
// composables/good.ts
export function useMyComposable() {
// Called inside the composable - has context
const config = useRuntimeConfig()
return config.public.apiBase
}Valid Contexts for Composables
Nuxt composables work in:
<script setup>blockssetup()functiondefineNuxtPlugin()callbacksdefineNuxtRouteMiddleware()callbacks
// ✅ Plugin
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig() // Works
})
// ✅ Middleware
export default defineNuxtRouteMiddleware(() => {
const route = useRoute() // Works
})Avoid State Leaks Between Requests
❌ Wrong: Module-level State
// composables/bad.ts
// This state is SHARED between all requests on server!
const globalState = ref({ user: null })
export function useUser() {
return globalState
}✅ Correct: Use useState
// composables/good.ts
export function useUser() {
// useState creates request-isolated state
return useState('user', () => ({ user: null }))
}Why This Matters
On the server, module-level state persists across requests, causing:
- Data leaking between users
- Security vulnerabilities
- Memory leaks
Hydration Mismatch Prevention
Hydration mismatches occur when server HTML differs from client render.
❌ Wrong: Browser APIs in Setup
<script setup>
// localStorage doesn't exist on server!
const theme = localStorage.getItem('theme') || 'light'
</script>✅ Correct: Use SSR-safe Alternatives
<script setup>
// useCookie works on both server and client
const theme = useCookie('theme', { default: () => 'light' })
</script>❌ Wrong: Random/Time-based Values
<template>
<div>{{ Math.random() }}</div>
<div>{{ new Date().toLocaleTimeString() }}</div>
</template>✅ Correct: Use useState for Consistency
<script setup>
// Value is generated once on server, hydrated on client
const randomValue = useState('random', () => Math.random())
</script>
<template>
<div>{{ randomValue }}</div>
</template>❌ Wrong: Conditional Rendering on Client State
<template>
<!-- window doesn't exist on server -->
<div v-if="window?.innerWidth > 768">Desktop</div>
</template>✅ Correct: Use CSS or ClientOnly
<template>
<!-- CSS media queries work on both -->
<div class="hidden md:block">Desktop</div>
<div class="md:hidden">Mobile</div>
<!-- Or use ClientOnly for JS-dependent rendering -->
<ClientOnly>
<ResponsiveComponent />
<template #fallback>Loading...</template>
</ClientOnly>
</template>Browser-only Code
Use import.meta.client
<script setup>
if (import.meta.client) {
// Only runs in browser
window.addEventListener('scroll', handleScroll)
}
</script>Use onMounted for DOM Access
<script setup>
const el = ref<HTMLElement>()
onMounted(() => {
// Safe - only runs on client after hydration
el.value?.focus()
initThirdPartyLib()
})
</script>Dynamic Imports for Browser Libraries
<script setup>
onMounted(async () => {
const { Chart } = await import('chart.js')
new Chart(canvas.value, config)
})
</script>Server-only Code
Use import.meta.server
<script setup>
if (import.meta.server) {
// Only runs on server
const secrets = useRuntimeConfig().apiSecret
}
</script>Server Components
<!-- components/ServerData.server.vue -->
<script setup>
// This entire component only runs on server
const data = await fetchSensitiveData()
</script>
<template>
<div>{{ data }}</div>
</template>Async Composable Patterns
❌ Wrong: Await Before Composable
<script setup>
await someAsyncOperation()
const route = useRoute() // May fail - context lost after await
</script>✅ Correct: Get Context First
<script setup>
// Get all composables before any await
const route = useRoute()
const config = useRuntimeConfig()
await someAsyncOperation()
// Now safe to use route and config
</script>Plugin Best Practices
Client-only Plugins
// plugins/analytics.client.ts
export default defineNuxtPlugin(() => {
// Only runs on client
initAnalytics()
})Server-only Plugins
// plugins/server-init.server.ts
export default defineNuxtPlugin(() => {
// Only runs on server
initServerConnections()
})Provide/Inject Pattern
// plugins/api.ts
export default defineNuxtPlugin(() => {
const api = createApiClient()
return {
provide: {
api,
},
}
})<script setup>
const { $api } = useNuxtApp()
const data = await $api.get('/users')
</script>Third-party Library Integration
❌ Wrong: Import at Top Level
<script setup>
import SomeLibrary from 'browser-only-lib' // Breaks SSR
</script>✅ Correct: Dynamic Import
<script setup>
let library: typeof import('browser-only-lib')
onMounted(async () => {
library = await import('browser-only-lib')
library.init()
})
</script>Use ClientOnly Component
<template>
<ClientOnly>
<BrowserOnlyComponent />
<template #fallback>
<div class="skeleton">Loading...</div>
</template>
</ClientOnly>
</template>Debugging SSR Issues
Check Rendering Context
<script setup>
console.log('Server:', import.meta.server)
console.log('Client:', import.meta.client)
</script>Use Nuxt DevTools
DevTools shows payload data and hydration state.
Common Error Messages
| Error | Cause |
|---|---|
| "Nuxt instance unavailable" | Composable called outside setup context |
| "Hydration mismatch" | Server/client HTML differs |
| "window is not defined" | Browser API used during SSR |
| "document is not defined" | DOM access during SSR |
<!-- Source references:
- https://nuxt.com/docs/guide/concepts/auto-imports#vue-and-nuxt-composables
- https://nuxt.com/docs/guide/best-practices/hydration
- https://nuxt.com/docs/getting-started/state-management#best-practices
-->
CLI Commands
Nuxt provides CLI commands via nuxi (or npx nuxt) for development, building, and project management.
Project Initialization
Create New Project
# Interactive project creation
npx nuxi@latest init my-app
# With specific package manager
npx nuxi@latest init my-app --packageManager pnpm
# With modules
npx nuxi@latest init my-app --modules "@nuxt/ui,@nuxt/image"
# From template
npx nuxi@latest init my-app --template v3
# Skip module selection prompt
npx nuxi@latest init my-app --no-modulesOptions:
| Option | Description |
|---|---|
-t, --template | Template name |
--packageManager | npm, pnpm, yarn, or bun |
-M, --modules | Modules to install (comma-separated) |
--gitInit | Initialize git repository |
--no-install | Skip installing dependencies |
Development
Start Dev Server
# Start development server (default: http://localhost:3000)
npx nuxt dev
# Custom port
npx nuxt dev --port 4000
# Open in browser
npx nuxt dev --open
# Listen on all interfaces (for mobile testing)
npx nuxt dev --host 0.0.0.0
# With HTTPS
npx nuxt dev --https
# Clear console on restart
npx nuxt dev --clear
# Create public tunnel
npx nuxt dev --tunnelOptions:
| Option | Description |
|---|---|
-p, --port | Port to listen on |
-h, --host | Host to listen on |
-o, --open | Open in browser |
--https | Enable HTTPS |
--tunnel | Create public tunnel (via untun) |
--qr | Show QR code for mobile |
--clear | Clear console on restart |
Environment Variables:
NUXT_PORTorPORT- Default portNUXT_HOSTorHOST- Default host
Building
Production Build
# Build for production
npx nuxt build
# Build with prerendering
npx nuxt build --prerender
# Build with specific preset
npx nuxt build --preset node-server
npx nuxt build --preset cloudflare-pages
npx nuxt build --preset vercel
# Build with environment
npx nuxt build --envName stagingOutput is created in .output/ directory.
Static Generation
# Generate static site (prerenders all routes)
npx nuxt generateEquivalent to nuxt build --prerender. Creates static HTML files for deployment to static hosting.
Preview Production Build
# Preview after build
npx nuxt preview
# Custom port
npx nuxt preview --port 4000Utilities
Prepare (Type Generation)
# Generate TypeScript types and .nuxt directory
npx nuxt prepareRun after cloning or when types are missing.
Type Check
# Run TypeScript type checking
npx nuxt typecheckAnalyze Bundle
# Analyze production bundle
npx nuxt analyzeOpens visual bundle analyzer.
Cleanup
# Remove generated files (.nuxt, .output, node_modules/.cache)
npx nuxt cleanupInfo
# Show environment info (useful for bug reports)
npx nuxt infoUpgrade
# Upgrade Nuxt to latest version
npx nuxt upgrade
# Upgrade to nightly release
npx nuxt upgrade --nightlyModule Commands
Add Module
# Add a Nuxt module
npx nuxt module add @nuxt/ui
npx nuxt module add @nuxt/imageInstalls and adds to nuxt.config.ts.
Build Module (for module authors)
# Build a Nuxt module
npx nuxt build-moduleDevTools
# Enable DevTools globally
npx nuxt devtools enable
# Disable DevTools
npx nuxt devtools disableCommon Workflows
Development
# Install dependencies and start dev
pnpm install
pnpm dev # or npx nuxt devProduction Deployment
# Build and preview locally
pnpm build
pnpm preview
# Or for static hosting
pnpm generateAfter Cloning
# Install deps and prepare types
pnpm install
npx nuxt prepareEnvironment-specific Builds
# Development build
npx nuxt build --envName development
# Staging build
npx nuxt build --envName staging
# Production build (default)
npx nuxt build --envName productionCorresponds to $development, $env.staging, $production in nuxt.config.ts.
Layer Extension
# Dev with additional layer
npx nuxt dev --extends ./base-layer
# Build with layer
npx nuxt build --extends ./base-layer<!-- Source references:
- https://nuxt.com/docs/api/commands/dev
- https://nuxt.com/docs/api/commands/build
- https://nuxt.com/docs/api/commands/generate
- https://nuxt.com/docs/api/commands/init
-->
Nuxt Configuration
Nuxt uses configuration files to customize application behavior. The main configuration options are nuxt.config.ts for build-time settings and app.config.ts for runtime settings.
nuxt.config.ts
The main configuration file at the root of your project:
// nuxt.config.ts
export default defineNuxtConfig({
// Configuration options
devtools: { enabled: true },
modules: ['@nuxt/ui'],
})Environment Overrides
Configure environment-specific settings:
export default defineNuxtConfig({
$production: {
routeRules: {
'/**': { isr: true },
},
},
$development: {
// Development-specific config
},
$env: {
staging: {
// Staging environment config
},
},
})Use --envName flag to select environment: nuxt build --envName staging
Runtime Config
For values that need to be overridden via environment variables:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Server-only keys
apiSecret: '123',
// Keys within public are exposed to client
public: {
apiBase: '/api',
},
},
})Override with environment variables:
# .env
NUXT_API_SECRET=api_secret_token
NUXT_PUBLIC_API_BASE=https://api.example.comAccess in components/composables:
<script setup lang="ts">
const config = useRuntimeConfig()
// Server: config.apiSecret, config.public.apiBase
// Client: config.public.apiBase only
</script>App Config
For public tokens determined at build time (not overridable via env vars):
// app/app.config.ts
export default defineAppConfig({
title: 'Hello Nuxt',
theme: {
dark: true,
colors: {
primary: '#ff0000',
},
},
})Access in components:
<script setup lang="ts">
const appConfig = useAppConfig()
</script>runtimeConfig vs app.config
| Feature | runtimeConfig | app.config |
|---|---|---|
| Client-side | Hydrated | Bundled |
| Environment variables | Yes | No |
| Reactive | Yes | Yes |
| Hot module replacement | No | Yes |
| Non-primitive JS types | No | Yes |
Use runtimeConfig for secrets and values that change per environment. Use app.config for public tokens, theme settings, and non-sensitive config.
External Tool Configuration
Nuxt uses nuxt.config.ts as single source of truth. Configure external tools within it:
export default defineNuxtConfig({
// Nitro configuration
nitro: {
// nitro options
},
// Vite configuration
vite: {
// vite options
vue: {
// @vitejs/plugin-vue options
},
},
// PostCSS configuration
postcss: {
// postcss options
},
})Vue Configuration
Enable Vue experimental features:
export default defineNuxtConfig({
vue: {
propsDestructure: true,
},
})<!-- Source references:
- https://nuxt.com/docs/getting-started/configuration
- https://nuxt.com/docs/guide/going-further/runtime-config
- https://nuxt.com/docs/api/nuxt-config
-->
Data Fetching
Nuxt provides composables for SSR-friendly data fetching that prevent double-fetching and handle hydration.
Overview
$fetch- Basic fetch utility (use for client-side events)useFetch- SSR-safe wrapper around $fetch (use for component data)useAsyncData- SSR-safe wrapper for any async function
useFetch
Primary composable for fetching data in components:
<script setup lang="ts">
const { data, status, error, refresh, clear } = await useFetch('/api/posts')
</script>
<template>
<div v-if="status === 'pending'">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>
<article v-for="post in data" :key="post.id">
{{ post.title }}
</article>
</div>
</template>With Options
const { data } = await useFetch('/api/posts', {
// Query parameters
query: { page: 1, limit: 10 },
// Request body (for POST/PUT)
body: { title: 'New Post' },
// HTTP method
method: 'POST',
// Only pick specific fields
pick: ['id', 'title'],
// Transform response
transform: (posts) => posts.map(p => ({ ...p, slug: slugify(p.title) })),
// Custom key for caching
key: 'posts-list',
// Don't fetch on server
server: false,
// Don't block navigation
lazy: true,
// Don't fetch immediately
immediate: false,
// Default value
default: () => [],
})Reactive Parameters
<script setup lang="ts">
const page = ref(1)
const { data } = await useFetch('/api/posts', {
query: { page }, // Automatically refetches when page changes
})
</script>Computed URL
<script setup lang="ts">
const id = ref(1)
const { data } = await useFetch(() => `/api/posts/${id.value}`)
// Refetches when id changes
</script>useAsyncData
For wrapping any async function:
<script setup lang="ts">
const { data, error } = await useAsyncData('user', () => {
return myCustomFetch('/user/profile')
})
</script>Multiple Requests
<script setup lang="ts">
const { data } = await useAsyncData('cart', async () => {
const [coupons, offers] = await Promise.all([
$fetch('/api/coupons'),
$fetch('/api/offers'),
])
return { coupons, offers }
})
</script>$fetch
For client-side events (form submissions, button clicks):
<script setup lang="ts">
async function submitForm() {
const result = await $fetch('/api/submit', {
method: 'POST',
body: { name: 'John' },
})
}
</script>Important: Don't use $fetch alone in setup for initial data - it will fetch twice (server + client). Use useFetch or useAsyncData instead.
Return Values
All composables return:
| Property | Type | Description |
|---|---|---|
data | Ref<T> | Fetched data |
error | Ref<Error> | Error if request failed |
status | `Ref<'idle' \ | 'pending' \ |
refresh | () => Promise | Refetch data |
execute | () => Promise | Alias for refresh |
clear | () => void | Reset data and error |
Lazy Fetching
Don't block navigation:
<script setup lang="ts">
// Using lazy option
const { data, status } = await useFetch('/api/posts', { lazy: true })
// Or use lazy variants
const { data, status } = await useLazyFetch('/api/posts')
const { data, status } = await useLazyAsyncData('key', fetchFn)
</script>Refresh & Watch
<script setup lang="ts">
const category = ref('tech')
const { data, refresh } = await useFetch('/api/posts', {
query: { category },
// Auto-refresh when category changes
watch: [category],
})
// Manual refresh
const refreshData = () => refresh()
</script>Caching
Data is cached by key. Share data across components:
<script setup lang="ts">
// In component A
const { data } = await useFetch('/api/user', { key: 'current-user' })
// In component B - uses cached data
const { data } = useNuxtData('current-user')
</script>Refresh cached data globally:
// Refresh specific key
await refreshNuxtData('current-user')
// Refresh all data
await refreshNuxtData()
// Clear cached data
clearNuxtData('current-user')Interceptors
const { data } = await useFetch('/api/auth', {
onRequest({ options }) {
options.headers.set('Authorization', `Bearer ${token}`)
},
onRequestError({ error }) {
console.error('Request failed:', error)
},
onResponse({ response }) {
// Process response
},
onResponseError({ response }) {
if (response.status === 401) {
navigateTo('/login')
}
},
})Passing Headers (SSR)
useFetch automatically proxies cookies/headers from client to server. For $fetch:
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
const data = await $fetch('/api/user', { headers })
</script><!-- Source references:
- https://nuxt.com/docs/getting-started/data-fetching
- https://nuxt.com/docs/api/composables/use-fetch
- https://nuxt.com/docs/api/composables/use-async-data
-->
Deployment
Nuxt is platform-agnostic thanks to Nitro, its server engine. You can deploy to almost any platform with minimal configuration—Node.js servers, static hosting, serverless functions, or edge networks.
Full list of supported platforms: https://nitro.build/deploy
Deployment Modes
Node.js Server
# Build for Node.js
nuxt build
# Run production server
node .output/server/index.mjsEnvironment variables:
PORTorNITRO_PORT(default: 3000)HOSTorNITRO_HOST(default: 0.0.0.0)
Static Generation
# Generate static site
nuxt generateOutput in .output/public/ - deploy to any static host.
Preset Configuration
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'vercel', // or 'netlify', 'cloudflare-pages', etc.
},
})Or via environment variable:
NITRO_PRESET=vercel nuxt build---
Recommended Platforms
When helping users choose a deployment platform, consider their needs:
Vercel
Best for: Projects wanting zero-config deployment with excellent DX
# Install Vercel CLI
npm i -g vercel
# Deploy
vercelPros:
- Zero configuration for Nuxt (auto-detects)
- Excellent preview deployments for PRs
- Built-in analytics and speed insights
- Edge Functions support
- Great free tier for personal projects
Cons:
- Can get expensive at scale (bandwidth costs)
- Vendor lock-in concerns
- Limited build minutes on free tier
Recommended when: User wants fastest setup, values DX, building SaaS or marketing sites.
---
Netlify
Best for: JAMstack sites, static-heavy apps, teams needing forms/identity
# Install Netlify CLI
npm i -g netlify-cli
# Deploy
netlify deploy --prodPros:
- Great free tier with generous bandwidth
- Built-in forms, identity, and functions
- Excellent for static sites with some dynamic features
- Good preview deployments
- Split testing built-in
Cons:
- SSR/serverless functions can be slower than Vercel
- Less optimized for full SSR apps
- Build minutes can run out on free tier
Recommended when: User has static-heavy site, needs built-in forms/auth, or prefers Netlify ecosystem.
---
Cloudflare Pages
Best for: Global performance, edge computing, cost-conscious projects
# Build with Cloudflare preset
NITRO_PRESET=cloudflare-pages nuxt buildPros:
- Unlimited bandwidth on free tier
- Excellent global edge network (fastest TTFB)
- Workers for edge computing
- Very cost-effective at scale
- D1, KV, R2 for data storage
Cons:
- Workers have execution limits (CPU time)
- Some Node.js APIs not available in Workers
- Less mature than Vercel/Netlify for frameworks
Recommended when: User prioritizes performance, global reach, or cost at scale.
---
GitHub Actions + Self-hosted/VPS
Best for: Full control, existing infrastructure, CI/CD customization
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
# Deploy to your server (example: rsync to VPS)
- name: Deploy to server
run: rsync -avz .output/ user@server:/app/Pros:
- Full control over build and deployment
- No vendor lock-in
- Can deploy anywhere (VPS, Docker, Kubernetes)
- Free CI/CD minutes for public repos
- Customizable workflows
Cons:
- Requires more setup and maintenance
- Need to manage your own infrastructure
- No built-in preview deployments
- SSL, scaling, monitoring are your responsibility
Recommended when: User has existing infrastructure, needs full control, or deploying to private/enterprise environments.
---
Quick Decision Guide
| Need | Recommendation |
|---|---|
| Fastest setup, small team | Vercel |
| Static site with forms | Netlify |
| Cost-sensitive at scale | Cloudflare Pages |
| Full control / enterprise | GitHub Actions + VPS |
| Docker/Kubernetes | GitHub Actions + Container Registry |
| Serverless APIs | Vercel or AWS Lambda |
Docker Deployment
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.output .output
ENV PORT=3000
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]docker build -t my-nuxt-app .
docker run -p 3000:3000 my-nuxt-app<!-- Source references:
- https://nuxt.com/docs/getting-started/deployment
- https://nitro.build/deploy
-->
Directory Structure
Nuxt uses conventions-based directory structure. Understanding it is key to effective development.
Standard Project Structure
my-nuxt-app/
├── app/ # Application source (can be at root level)
│ ├── app.vue # Root component
│ ├── app.config.ts # App configuration (runtime)
│ ├── error.vue # Error page
│ ├── components/ # Auto-imported Vue components
│ ├── composables/ # Auto-imported composables
│ ├── layouts/ # Layout components
│ ├── middleware/ # Route middleware
│ ├── pages/ # File-based routing
│ ├── plugins/ # Vue plugins
│ └── utils/ # Auto-imported utilities
├── assets/ # Build-processed assets (CSS, images)
├── public/ # Static assets (served as-is)
├── server/ # Server-side code
│ ├── api/ # API routes (/api/*)
│ ├── routes/ # Server routes
│ ├── middleware/ # Server middleware
│ ├── plugins/ # Nitro plugins
│ └── utils/ # Server utilities (auto-imported)
├── content/ # Content files (@nuxt/content)
├── layers/ # Local layers (auto-scanned)
├── modules/ # Local modules
├── nuxt.config.ts # Nuxt configuration
├── package.json
└── tsconfig.jsonKey Directories
app/ Directory
Contains all application code. Can also be at root level (without app/ folder).
// nuxt.config.ts - customize source directory
export default defineNuxtConfig({
srcDir: 'src/', // Change from 'app/' to 'src/'
})app/components/
Vue components auto-imported by name:
components/
├── Button.vue → <Button />
├── Card.vue → <Card />
├── base/
│ └── Button.vue → <BaseButton />
├── ui/
│ ├── Input.vue → <UiInput />
│ └── Modal.vue → <UiModal />
└── TheHeader.vue → <TheHeader />Lazy loading: Prefix with Lazy for dynamic import:
<template>
<LazyHeavyChart v-if="showChart" />
</template>Client/Server only:
components/
├── Comments.client.vue → Only rendered on client
└── ServerData.server.vue → Only rendered on serverapp/composables/
Vue composables auto-imported (top-level files only):
composables/
├── useAuth.ts → useAuth()
├── useFoo.ts → useFoo()
└── nested/
└── utils.ts → NOT auto-importedRe-export nested composables:
// composables/index.ts
export { useHelper } from './nested/utils'app/pages/
File-based routing:
pages/
├── index.vue → /
├── about.vue → /about
├── blog/
│ ├── index.vue → /blog
│ └── [slug].vue → /blog/:slug
├── users/
│ └── [id]/
│ └── profile.vue → /users/:id/profile
├── [...slug].vue → /* (catch-all)
├── [[optional]].vue → /:optional? (optional param)
└── (marketing)/ → Route group (not in URL)
└── pricing.vue → /pricingPages are optional: Without pages/, no vue-router is included.
app/layouts/
Layout components wrapping pages:
layouts/
├── default.vue → Default layout
├── admin.vue → Admin layout
└── blank.vue → No layout<!-- layouts/default.vue -->
<template>
<div>
<TheHeader />
<slot />
<TheFooter />
</div>
</template>Use in pages:
<script setup>
definePageMeta({
layout: 'admin',
// layout: false // Disable layout
})
</script>app/middleware/
Route middleware:
middleware/
├── auth.ts → Named middleware
├── admin.ts → Named middleware
└── logger.global.ts → Global middleware (runs on every route)app/plugins/
Nuxt plugins (auto-registered):
plugins/
├── 01.analytics.ts → Order with number prefix
├── 02.auth.ts
├── vue-query.client.ts → Client-only plugin
└── server-init.server.ts → Server-only pluginserver/ Directory
Nitro server code:
server/
├── api/
│ ├── users.ts → GET /api/users
│ ├── users.post.ts → POST /api/users
│ └── users/[id].ts → /api/users/:id
├── routes/
│ └── sitemap.xml.ts → /sitemap.xml
├── middleware/
│ └── auth.ts → Runs on every request
├── plugins/
│ └── db.ts → Server startup plugins
└── utils/
└── db.ts → Auto-imported server utilitiespublic/ Directory
Static assets served at root URL:
public/
├── favicon.ico → /favicon.ico
├── robots.txt → /robots.txt
└── images/
└── logo.png → /images/logo.pngassets/ Directory
Build-processed assets:
assets/
├── css/
│ └── main.css
├── images/
│ └── hero.png
└── fonts/
└── custom.woff2Reference in components:
<template>
<img src="~/assets/images/hero.png" />
</template>
<style>
@import '~/assets/css/main.css';
</style>Special Files
| File | Purpose |
|---|---|
app.vue | Root component (optional with pages/) |
app.config.ts | Runtime app configuration |
error.vue | Custom error page |
nuxt.config.ts | Build-time configuration |
.nuxtignore | Ignore files from Nuxt |
.env | Environment variables |
File Naming Conventions
| Pattern | Meaning |
|---|---|
[param] | Dynamic route parameter |
[[param]] | Optional parameter |
[...slug] | Catch-all route |
(group) | Route group (not in URL) |
.client.vue | Client-only component |
.server.vue | Server-only component |
.global.ts | Global middleware |
<!-- Source references:
- https://nuxt.com/docs/directory-structure
- https://nuxt.com/docs/directory-structure/app
- https://nuxt.com/docs/directory-structure/server
-->
Nuxt Modules
Modules extend Nuxt's core functionality. They run at build time and can add components, composables, plugins, and configuration.
Using Modules
Install and add to nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
// npm package
'@nuxt/ui',
// Local module
'./modules/my-module',
// Inline module
(options, nuxt) => {
console.log('Inline module')
},
// With options
['@nuxt/image', { provider: 'cloudinary' }],
],
})Creating Modules
Basic Module
// modules/my-module.ts
export default defineNuxtModule({
meta: {
name: 'my-module',
configKey: 'myModule',
},
defaults: {
enabled: true,
},
setup(options, nuxt) {
if (!options.enabled) return
console.log('My module is running!')
},
})Adding Components
// modules/ui/index.ts
import { addComponent, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
// Add single component
addComponent({
name: 'MyButton',
filePath: resolve('./runtime/components/MyButton.vue'),
})
// Add components directory
addComponentsDir({
path: resolve('./runtime/components'),
prefix: 'My',
})
},
})Adding Composables
// modules/utils/index.ts
import { addImports, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
// Add auto-imported composable
addImports({
name: 'useMyUtil',
from: resolve('./runtime/composables/useMyUtil'),
})
// Add directory for auto-imports
addImportsDir(resolve('./runtime/composables'))
},
})Adding Plugins
// modules/analytics/index.ts
import { addPlugin, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
addPlugin({
src: resolve('./runtime/plugin'),
mode: 'client', // 'client', 'server', or 'all'
})
},
})Plugin file:
// modules/analytics/runtime/plugin.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('page:finish', () => {
console.log('Page loaded')
})
})Adding Server Routes
// modules/api/index.ts
import { addServerHandler, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
addServerHandler({
route: '/api/my-endpoint',
handler: resolve('./runtime/server/api/my-endpoint'),
})
},
})Extending Config
// modules/config/index.ts
export default defineNuxtModule({
setup(options, nuxt) {
// Add CSS
nuxt.options.css.push('my-module/styles.css')
// Add runtime config
nuxt.options.runtimeConfig.public.myModule = {
apiUrl: options.apiUrl,
}
// Extend Vite config
nuxt.options.vite.optimizeDeps ||= {}
nuxt.options.vite.optimizeDeps.include ||= []
nuxt.options.vite.optimizeDeps.include.push('some-package')
},
})Module Hooks
export default defineNuxtModule({
setup(options, nuxt) {
// Build-time hooks
nuxt.hook('modules:done', () => {
console.log('All modules loaded')
})
nuxt.hook('components:dirs', (dirs) => {
dirs.push({ path: '~/extra-components' })
})
nuxt.hook('pages:extend', (pages) => {
pages.push({
name: 'custom-page',
path: '/custom',
file: resolve('./runtime/pages/custom.vue'),
})
})
nuxt.hook('imports:extend', (imports) => {
imports.push({ name: 'myHelper', from: 'my-package' })
})
},
})Module Options
Type-safe options with defaults:
export interface ModuleOptions {
apiKey: string
enabled?: boolean
prefix?: string
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'my-module',
configKey: 'myModule',
},
defaults: {
enabled: true,
prefix: 'My',
},
setup(options, nuxt) {
// options is typed as ModuleOptions
if (!options.apiKey) {
console.warn('API key not provided')
}
},
})Usage:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['my-module'],
myModule: {
apiKey: 'xxx',
prefix: 'Custom',
},
})Local Modules
Place in modules/ directory:
modules/
├── my-module/
│ ├── index.ts
│ └── runtime/
│ ├── components/
│ ├── composables/
│ └── plugin.tsAuto-registered or manually added:
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'~/modules/my-module', // Explicit
],
})Module Dependencies
export default defineNuxtModule({
meta: {
name: 'my-module',
},
moduleDependencies: {
'@nuxt/image': {
version: '>=1.0.0',
defaults: {
provider: 'ipx',
},
},
},
setup() {
// @nuxt/image is guaranteed to be installed
},
})<!-- Source references:
- https://nuxt.com/docs/guide/modules
- https://nuxt.com/docs/guide/modules/module-anatomy
- https://nuxt.com/docs/api/kit
-->
Routing
Nuxt uses file-system routing based on vue-router. Files in app/pages/ automatically create routes.
Basic Routing
pages/
├── index.vue → /
├── about.vue → /about
└── posts/
├── index.vue → /posts
└── [id].vue → /posts/:idDynamic Routes
Use brackets for dynamic segments:
pages/
├── users/
│ └── [id].vue → /users/:id
├── posts/
│ └── [...slug].vue → /posts/* (catch-all)
└── [[optional]].vue → /:optional? (optional param)Access route parameters:
<script setup lang="ts">
const route = useRoute()
// /posts/123 → route.params.id = '123'
console.log(route.params.id)
</script>Navigation
NuxtLink Component
<template>
<nav>
<NuxtLink to="/">Home</NuxtLink>
<NuxtLink to="/about">About</NuxtLink>
<NuxtLink :to="{ name: 'posts-id', params: { id: 1 } }">Post 1</NuxtLink>
</nav>
</template>NuxtLink automatically prefetches linked pages when they enter the viewport.
Programmatic Navigation
<script setup lang="ts">
const router = useRouter()
function goToPost(id: number) {
navigateTo(`/posts/${id}`)
// or
router.push({ name: 'posts-id', params: { id } })
}
</script>Route Middleware
Named Middleware
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const isAuthenticated = false // Your auth logic
if (!isAuthenticated) {
return navigateTo('/login')
}
})Apply to pages:
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
// or multiple: middleware: ['auth', 'admin']
})
</script>Global Middleware
Name files with .global suffix:
// middleware/logging.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
console.log('Navigating to:', to.path)
})Inline Middleware
<script setup lang="ts">
definePageMeta({
middleware: [
function (to, from) {
// Inline middleware logic
},
],
})
</script>Page Meta
Configure page-level options:
<script setup lang="ts">
definePageMeta({
title: 'My Page',
layout: 'custom',
middleware: 'auth',
validate: (route) => {
// Return false for 404, or object with status/statusText
return /^\d+$/.test(route.params.id as string)
},
})
</script>Route Validation
<script setup lang="ts">
definePageMeta({
validate: (route) => {
// Must return boolean or object with status
return typeof route.params.id === 'string' && /^\d+$/.test(route.params.id)
},
})
</script>Layouts
Define layouts in app/layouts/:
<!-- layouts/default.vue -->
<template>
<div>
<header>Header</header>
<slot />
<footer>Footer</footer>
</div>
</template><!-- layouts/admin.vue -->
<template>
<div class="admin">
<AdminSidebar />
<main>
<slot />
</main>
</div>
</template>Use in pages:
<script setup lang="ts">
definePageMeta({
layout: 'admin',
})
</script>Dynamic layout:
<script setup lang="ts">
const layout = ref('default')
function enableAdmin() {
setPageLayout('admin')
}
</script>Navigation Hooks
<script setup lang="ts">
onBeforeRouteLeave((to, from) => {
// Confirm before leaving
const answer = window.confirm('Leave?')
if (!answer) return false
})
onBeforeRouteUpdate((to, from) => {
// Called when route changes but component is reused
})
</script><!-- Source references:
- https://nuxt.com/docs/getting-started/routing
- https://nuxt.com/docs/directory-structure/app/pages
- https://nuxt.com/docs/directory-structure/app/middleware
-->
Components Auto-imports
Nuxt automatically imports Vue components from app/components/ directory.
Basic Auto-imports
components/
├── Button.vue → <Button />
├── Card.vue → <Card />
└── AppHeader.vue → <AppHeader /><template>
<!-- No imports needed -->
<AppHeader />
<Card>
<Button>Click me</Button>
</Card>
</template>Naming Conventions
Nested Directory Names
Component names include directory path:
components/
├── base/
│ └── Button.vue → <BaseButton />
├── form/
│ ├── Input.vue → <FormInput />
│ └── Select.vue → <FormSelect />
└── ui/
└── modal/
└── Dialog.vue → <UiModalDialog />Disable Path Prefix
// nuxt.config.ts
export default defineNuxtConfig({
components: [
{
path: '~/components',
pathPrefix: false, // Use filename only
},
],
})With pathPrefix: false:
components/base/Button.vue → <Button />Lazy Loading
Prefix with Lazy for dynamic imports:
<script setup lang="ts">
const showChart = ref(false)
</script>
<template>
<!-- Component code loaded only when rendered -->
<LazyHeavyChart v-if="showChart" />
<button @click="showChart = true">Show Chart</button>
</template>Benefits:
- Reduces initial bundle size
- Code-splits component into separate chunk
- Loads on-demand
Lazy Hydration Strategies
Control when lazy components become interactive:
hydrate-on-visible
Hydrate when component enters viewport:
<template>
<LazyComments hydrate-on-visible />
</template>hydrate-on-idle
Hydrate when browser is idle:
<template>
<LazyAnalytics hydrate-on-idle />
</template>hydrate-on-interaction
Hydrate on user interaction:
<template>
<!-- Hydrates on click, focus, or pointerenter -->
<LazyDropdown hydrate-on-interaction />
<!-- Specific event -->
<LazyTooltip hydrate-on-interaction="mouseover" />
</template>hydrate-on-media-query
Hydrate when media query matches:
<template>
<LazyMobileMenu hydrate-on-media-query="(max-width: 768px)" />
</template>hydrate-after
Hydrate after delay (milliseconds):
<template>
<LazyAds :hydrate-after="3000" />
</template>hydrate-when
Hydrate on condition:
<script setup lang="ts">
const isReady = ref(false)
</script>
<template>
<LazyEditor :hydrate-when="isReady" />
</template>hydrate-never
Never hydrate (static only):
<template>
<LazyStaticFooter hydrate-never />
</template>Hydration Event
<template>
<LazyChart hydrate-on-visible @hydrated="onChartReady" />
</template>
<script setup>
function onChartReady() {
console.log('Chart is now interactive')
}
</script>Client/Server Components
Client-only (.client.vue)
components/
└── BrowserChart.client.vue<template>
<!-- Only rendered in browser -->
<BrowserChart />
</template>Server-only (.server.vue)
components/
└── ServerMarkdown.server.vue<template>
<!-- Rendered on server, not hydrated -->
<ServerMarkdown :content="markdown" />
</template>Requires experimental flag:
// nuxt.config.ts
export default defineNuxtConfig({
experimental: {
componentIslands: true,
},
})Paired Components
components/
├── Comments.client.vue # Browser version
└── Comments.server.vue # SSR versionServer version renders during SSR, client version takes over after hydration.
Dynamic Components
<script setup lang="ts">
import { SomeComponent } from '#components'
const dynamicComponent = resolveComponent('MyButton')
</script>
<template>
<component :is="dynamicComponent" />
<component :is="SomeComponent" />
</template>Direct Imports
Bypass auto-imports when needed:
<script setup lang="ts">
import { LazyMountainsList, NuxtLink } from '#components'
</script>Custom Directories
// nuxt.config.ts
export default defineNuxtConfig({
components: [
{ path: '~/components/ui', prefix: 'Ui' },
{ path: '~/components/forms', prefix: 'Form' },
'~/components', // Default, should come last
],
})Global Components
Register globally (creates async chunks):
// nuxt.config.ts
export default defineNuxtConfig({
components: {
global: true,
dirs: ['~/components'],
},
})Or use .global.vue suffix:
components/
└── Icon.global.vue → Available globallyDisabling Component Auto-imports
// nuxt.config.ts
export default defineNuxtConfig({
components: {
dirs: [], // Disable auto-imports
},
})Library Authors
Register components from npm package:
// my-ui-lib/nuxt.ts
import { addComponentsDir, createResolver, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const resolver = createResolver(import.meta.url)
addComponentsDir({
path: resolver.resolve('./components'),
prefix: 'MyUi',
})
},
})<!-- Source references:
- https://nuxt.com/docs/directory-structure/app/components
- https://nuxt.com/docs/guide/concepts/auto-imports#auto-imported-components
-->
Built-in Components
Nuxt provides several built-in components for common functionality.
NuxtLink
Optimized link component with prefetching:
<template>
<!-- Basic usage -->
<NuxtLink to="/about">About</NuxtLink>
<!-- With route object -->
<NuxtLink :to="{ name: 'posts-id', params: { id: 1 } }">Post 1</NuxtLink>
<!-- External link (opens in new tab) -->
<NuxtLink to="https://nuxt.com" external>Nuxt</NuxtLink>
<!-- Disable prefetching -->
<NuxtLink to="/heavy-page" :prefetch="false">Heavy Page</NuxtLink>
<!-- Replace history instead of push -->
<NuxtLink to="/page" replace>Replace</NuxtLink>
<!-- Custom active class -->
<NuxtLink
to="/dashboard"
active-class="text-primary"
exact-active-class="font-bold"
>
Dashboard
</NuxtLink>
</template>NuxtPage
Renders the current page component (used in layouts):
<!-- app/app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>With page transitions:
<template>
<NuxtPage :transition="{ name: 'fade', mode: 'out-in' }" />
</template>Pass props to page:
<template>
<NuxtPage :page-key="route.fullPath" :foobar="123" />
</template>NuxtLayout
Controls layout rendering:
<!-- app/app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>Dynamic layout:
<template>
<NuxtLayout :name="layout">
<NuxtPage />
</NuxtLayout>
</template>
<script setup>
const layout = computed(() => isAdmin ? 'admin' : 'default')
</script>Layout with transitions:
<template>
<NuxtLayout :transition="{ name: 'slide', mode: 'out-in' }">
<NuxtPage />
</NuxtLayout>
</template>NuxtLoadingIndicator
Progress bar for page navigation:
<!-- app/app.vue -->
<template>
<NuxtLoadingIndicator
color="repeating-linear-gradient(to right, #00dc82 0%, #34cdfe 50%, #0047e1 100%)"
:height="3"
:duration="2000"
:throttle="200"
/>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>NuxtErrorBoundary
Catch and handle errors in child components:
<template>
<NuxtErrorBoundary @error="handleError">
<ComponentThatMightFail />
<template #error="{ error, clearError }">
<div class="error">
<p>Something went wrong: {{ error.message }}</p>
<button @click="clearError">Try again</button>
</div>
</template>
</NuxtErrorBoundary>
</template>
<script setup>
function handleError(error) {
console.error('Error caught:', error)
}
</script>ClientOnly
Render content only on client-side:
<template>
<ClientOnly>
<!-- Browser-only component -->
<BrowserOnlyChart :data="chartData" />
<template #fallback>
<p>Loading chart...</p>
</template>
</ClientOnly>
</template>DevOnly
Render content only in development:
<template>
<DevOnly>
<DebugPanel />
</DevOnly>
</template>NuxtIsland
Server components (experimental):
<template>
<NuxtIsland name="HeavyComponent" :props="{ data: complexData }" />
</template>NuxtImg and NuxtPicture
Optimized images (requires @nuxt/image module):
<template>
<!-- Basic optimized image -->
<NuxtImg src="/images/hero.jpg" width="800" height="600" />
<!-- Responsive with srcset -->
<NuxtImg
src="/images/hero.jpg"
sizes="sm:100vw md:50vw lg:400px"
:modifiers="{ format: 'webp' }"
/>
<!-- Art direction with picture -->
<NuxtPicture
src="/images/hero.jpg"
:img-attrs="{ alt: 'Hero image' }"
/>
</template>Teleport
Render content outside component tree:
<template>
<button @click="showModal = true">Open Modal</button>
<Teleport to="body">
<div v-if="showModal" class="modal">
<p>Modal content</p>
<button @click="showModal = false">Close</button>
</div>
</Teleport>
</template>For SSR, use <ClientOnly> with Teleport:
<template>
<ClientOnly>
<Teleport to="#teleports">
<Modal />
</Teleport>
</ClientOnly>
</template>NuxtRouteAnnouncer
Accessibility: announces page changes to screen readers:
<!-- app/app.vue -->
<template>
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template><!-- Source references:
- https://nuxt.com/docs/api/components/nuxt-link
- https://nuxt.com/docs/api/components/nuxt-page
- https://nuxt.com/docs/api/components/nuxt-layout
- https://nuxt.com/docs/api/components/client-only
-->
Composables Auto-imports
Nuxt automatically imports Vue APIs, Nuxt composables, and your custom composables/utilities.
Built-in Auto-imports
Vue APIs
<script setup lang="ts">
// No imports needed - all auto-imported
const count = ref(0)
const doubled = computed(() => count.value * 2)
watch(count, (newVal) => {
console.log('Count changed:', newVal)
})
onMounted(() => {
console.log('Component mounted')
})
// Lifecycle hooks
onBeforeMount(() => {})
onUnmounted(() => {})
onBeforeUnmount(() => {})
// Reactivity
const state = reactive({ name: 'John' })
const shallow = shallowRef({ deep: 'object' })
</script>Nuxt Composables
<script setup lang="ts">
// All auto-imported
const route = useRoute()
const router = useRouter()
const config = useRuntimeConfig()
const appConfig = useAppConfig()
const nuxtApp = useNuxtApp()
// Data fetching
const { data } = await useFetch('/api/data')
const { data: asyncData } = await useAsyncData('key', () => fetchData())
// State
const state = useState('key', () => 'initial')
const cookie = useCookie('token')
// Head/SEO
useHead({ title: 'My Page' })
useSeoMeta({ description: 'Page description' })
// Request helpers (SSR)
const headers = useRequestHeaders()
const event = useRequestEvent()
const url = useRequestURL()
</script>Custom Composables (app/composables/)
Creating Composables
// composables/useCounter.ts
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}// composables/useAuth.ts
export function useAuth() {
const user = useState<User | null>('user', () => null)
const isLoggedIn = computed(() => !!user.value)
async function login(credentials: Credentials) {
user.value = await $fetch('/api/auth/login', {
method: 'POST',
body: credentials,
})
}
async function logout() {
await $fetch('/api/auth/logout', { method: 'POST' })
user.value = null
}
return { user, isLoggedIn, login, logout }
}Using Composables
<script setup lang="ts">
// Auto-imported - no import statement needed
const { count, increment } = useCounter(10)
const { user, isLoggedIn, login } = useAuth()
</script>File Scanning Rules
Only top-level files are scanned:
composables/
├── useAuth.ts → useAuth() ✓
├── useCounter.ts → useCounter() ✓
├── index.ts → exports ✓
└── nested/
└── helper.ts → NOT auto-imported ✗Re-export nested composables:
// composables/index.ts
export { useHelper } from './nested/helper'Or configure scanning:
// nuxt.config.ts
export default defineNuxtConfig({
imports: {
dirs: [
'composables',
'composables/**', // Scan all nested
],
},
})Utilities (app/utils/)
// utils/format.ts
export function formatDate(date: Date) {
return date.toLocaleDateString()
}
export function formatCurrency(amount: number) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount)
}<script setup lang="ts">
// Auto-imported
const date = formatDate(new Date())
const price = formatCurrency(99.99)
</script>Server Utils (server/utils/)
// server/utils/db.ts
export function useDb() {
return createDbConnection()
}
// server/utils/auth.ts
export function verifyToken(token: string) {
return jwt.verify(token, process.env.JWT_SECRET)
}// server/api/users.ts
export default defineEventHandler(() => {
const db = useDb() // Auto-imported
return db.query('SELECT * FROM users')
})Third-party Package Imports
// nuxt.config.ts
export default defineNuxtConfig({
imports: {
presets: [
{
from: 'vue-i18n',
imports: ['useI18n'],
},
{
from: 'date-fns',
imports: ['format', 'parseISO', 'differenceInDays'],
},
{
from: '@vueuse/core',
imports: ['useMouse', 'useWindowSize'],
},
],
},
})Explicit Imports
Use #imports alias when needed:
<script setup lang="ts">
import { ref, computed, useFetch } from '#imports'
</script>Composable Context Rules
Nuxt composables must be called in valid context:
// ❌ Wrong - module level
const config = useRuntimeConfig()
export function useMyComposable() {}// ✅ Correct - inside function
export function useMyComposable() {
const config = useRuntimeConfig()
return { apiBase: config.public.apiBase }
}Valid contexts:
<script setup>blocksetup()functiondefineNuxtPlugin()callbackdefineNuxtRouteMiddleware()callback
Disabling Auto-imports
// nuxt.config.ts
export default defineNuxtConfig({
// Disable all auto-imports
imports: {
autoImport: false,
},
// Or disable only directory scanning (keep Vue/Nuxt imports)
imports: {
scan: false,
},
})<!-- Source references:
- https://nuxt.com/docs/guide/concepts/auto-imports
- https://nuxt.com/docs/directory-structure/app/composables
- https://nuxt.com/docs/directory-structure/app/utils
-->
Server Routes
Nuxt includes Nitro server engine for building full-stack applications with API routes and server middleware.
API Routes
Create files in server/api/ directory:
// server/api/hello.ts
export default defineEventHandler((event) => {
return { message: 'Hello World' }
})Access at /api/hello.
HTTP Methods
// server/api/users.get.ts - GET /api/users
export default defineEventHandler(() => {
return getUsers()
})
// server/api/users.post.ts - POST /api/users
export default defineEventHandler(async (event) => {
const body = await readBody(event)
return createUser(body)
})
// server/api/users/[id].put.ts - PUT /api/users/:id
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const body = await readBody(event)
return updateUser(id, body)
})
// server/api/users/[id].delete.ts - DELETE /api/users/:id
export default defineEventHandler((event) => {
const id = getRouterParam(event, 'id')
return deleteUser(id)
})Route Parameters
// server/api/posts/[id].ts
export default defineEventHandler((event) => {
const id = getRouterParam(event, 'id')
return getPost(id)
})
// Catch-all: server/api/[...path].ts
export default defineEventHandler((event) => {
const path = getRouterParam(event, 'path')
return { path }
})Query Parameters
// server/api/search.ts
// GET /api/search?q=nuxt&page=1
export default defineEventHandler((event) => {
const query = getQuery(event)
// { q: 'nuxt', page: '1' }
return search(query.q, Number(query.page))
})Request Body
// server/api/submit.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validate and process body
return { success: true, data: body }
})Headers and Cookies
// server/api/auth.ts
export default defineEventHandler((event) => {
// Read headers
const auth = getHeader(event, 'authorization')
// Read cookies
const cookies = parseCookies(event)
const token = getCookie(event, 'token')
// Set headers
setHeader(event, 'X-Custom-Header', 'value')
// Set cookies
setCookie(event, 'token', 'new-token', {
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24, // 1 day
})
return { authenticated: !!token }
})Server Middleware
Runs on every request before routes:
// server/middleware/auth.ts
export default defineEventHandler((event) => {
const token = getCookie(event, 'token')
// Attach data to event context
event.context.user = token ? verifyToken(token) : null
})
// server/middleware/log.ts
export default defineEventHandler((event) => {
console.log(`${event.method} ${event.path}`)
})Access context in routes:
// server/api/profile.ts
export default defineEventHandler((event) => {
const user = event.context.user
if (!user) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
return user
})Error Handling
// server/api/users/[id].ts
export default defineEventHandler((event) => {
const id = getRouterParam(event, 'id')
const user = findUser(id)
if (!user) {
throw createError({
statusCode: 404,
statusMessage: 'User not found',
})
}
return user
})Server Utils
Auto-imported in server/utils/:
// server/utils/db.ts
export function useDb() {
return createDbConnection()
}// server/api/users.ts
export default defineEventHandler(() => {
const db = useDb() // Auto-imported
return db.query('SELECT * FROM users')
})Server Plugins
Run once when server starts:
// server/plugins/db.ts
export default defineNitroPlugin((nitroApp) => {
// Initialize database connection
const db = createDbConnection()
// Add to context
nitroApp.hooks.hook('request', (event) => {
event.context.db = db
})
})Streaming Responses
// server/api/stream.ts
export default defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'text/event-stream')
setHeader(event, 'Cache-Control', 'no-cache')
setHeader(event, 'Connection', 'keep-alive')
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
controller.enqueue(`data: ${JSON.stringify({ count: i })}\n\n`)
await new Promise(r => setTimeout(r, 1000))
}
controller.close()
},
})
return stream
})Server Storage
Key-value storage with multiple drivers:
// server/api/cache.ts
export default defineEventHandler(async (event) => {
const storage = useStorage()
// Set value
await storage.setItem('key', { data: 'value' })
// Get value
const data = await storage.getItem('key')
return data
})Configure storage drivers in nuxt.config.ts:
export default defineNuxtConfig({
nitro: {
storage: {
redis: {
driver: 'redis',
url: 'redis://localhost:6379',
},
},
},
})<!-- Source references:
- https://nuxt.com/docs/getting-started/server
- https://nuxt.com/docs/directory-structure/server
- https://nitro.build/guide
-->
State Management
Nuxt provides useState for SSR-friendly reactive state that persists across components.
useState
SSR-safe replacement for ref that shares state across components:
<script setup lang="ts">
// State is shared by key 'counter' across all components
const counter = useState('counter', () => 0)
</script>
<template>
<div>
Counter: {{ counter }}
<button @click="counter++">+</button>
<button @click="counter--">-</button>
</div>
</template>Creating Shared State
Define reusable state composables:
// composables/useUser.ts
export function useUser() {
return useState<User | null>('user', () => null)
}
export function useLocale() {
return useState('locale', () => 'en')
}<script setup lang="ts">
// Same state instance everywhere
const user = useUser()
const locale = useLocale()
</script>Initializing State
Use callOnce to initialize state with async data:
<script setup lang="ts">
const config = useState('site-config')
await callOnce(async () => {
config.value = await $fetch('/api/config')
})
</script>Best Practices
❌ Don't Define State Outside Setup
// ❌ Wrong - causes memory leaks and shared state across requests
export const globalState = ref({ user: null })✅ Use useState Instead
// ✅ Correct - SSR-safe
export const useGlobalState = () => useState('global', () => ({ user: null }))Clearing State
// Clear specific state
clearNuxtState('counter')
// Clear multiple states
clearNuxtState(['counter', 'user'])
// Clear all state (use with caution)
clearNuxtState()With Pinia
For complex state management, use Pinia:
npx nuxi module add pinia// stores/counter.ts
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
},
})// stores/user.ts (Composition API style)
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
const isLoggedIn = computed(() => !!user.value)
async function login(credentials: Credentials) {
user.value = await $fetch('/api/login', {
method: 'POST',
body: credentials,
})
}
return { user, isLoggedIn, login }
})<script setup lang="ts">
const counterStore = useCounterStore()
const userStore = useUserStore()
// Initialize store data once
await callOnce(async () => {
await userStore.fetchUser()
})
</script>Advanced: Locale Example
// composables/useLocale.ts
export function useLocale() {
return useState('locale', () => useDefaultLocale().value)
}
export function useDefaultLocale(fallback = 'en-US') {
const locale = ref(fallback)
if (import.meta.server) {
const reqLocale = useRequestHeaders()['accept-language']?.split(',')[0]
if (reqLocale) locale.value = reqLocale
}
else if (import.meta.client) {
const navLang = navigator.language
if (navLang) locale.value = navLang
}
return locale
}State Serialization
useState values are serialized to JSON. Avoid:
- Functions
- Classes
- Symbols
- Circular references
// ❌ Won't work
useState('fn', () => () => console.log('hi'))
useState('instance', () => new MyClass())
// ✅ Works
useState('data', () => ({ name: 'John', age: 30 }))
useState('items', () => ['a', 'b', 'c'])<!-- Source references:
- https://nuxt.com/docs/getting-started/state-management
- https://nuxt.com/docs/api/composables/use-state
- https://nuxt.com/docs/api/utils/clear-nuxt-state
-->
Rendering Modes
Nuxt supports multiple rendering modes: universal (SSR), client-side (CSR), and hybrid rendering.
Universal Rendering (Default)
Server renders HTML, then hydrates on client:
// nuxt.config.ts - this is the default
export default defineNuxtConfig({
ssr: true,
})Benefits:
- Fast initial page load (HTML is ready)
- SEO-friendly (content is in HTML)
- Works without JavaScript initially
How it works: 1. Server executes Vue code, generates HTML 2. Browser displays HTML immediately 3. JavaScript loads and hydrates the page 4. Page becomes fully interactive
Client-Side Rendering
Render entirely in the browser:
// nuxt.config.ts
export default defineNuxtConfig({
ssr: false,
})Benefits:
- Simpler development (no SSR constraints)
- Cheaper hosting (static files only)
- Works offline
Use cases:
- Admin dashboards
- SaaS applications
- Apps behind authentication
SPA Loading Template
Provide loading UI while app hydrates:
<!-- app/spa-loading-template.html -->
<div class="loading">
<div class="spinner"></div>
<p>Loading...</p>
</div>
<style>
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #00dc82;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>Hybrid Rendering
Mix rendering modes per route using route rules:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Static pages - prerendered at build
'/': { prerender: true },
'/about': { prerender: true },
// ISR - regenerate in background
'/blog/**': { isr: 3600 }, // Cache for 1 hour
'/products/**': { swr: true }, // Stale-while-revalidate
// Client-only rendering
'/admin/**': { ssr: false },
'/dashboard/**': { ssr: false },
// Server-rendered (default)
'/api/**': { cors: true },
},
})Route Rules Reference
| Rule | Description |
|---|---|
prerender: true | Pre-render at build time |
ssr: false | Client-side only |
| `swr: number \ | true` |
| `isr: number \ | true` |
cache: { maxAge: number } | Cache with TTL |
redirect: string | Redirect to another path |
cors: true | Add CORS headers |
headers: object | Custom response headers |
Inline Route Rules
Define per-page:
<script setup lang="ts">
defineRouteRules({
prerender: true,
})
</script>Prerendering
Generate static HTML at build time:
// nuxt.config.ts
export default defineNuxtConfig({
// Prerender specific routes
routeRules: {
'/': { prerender: true },
'/about': { prerender: true },
'/posts/*': { prerender: true },
},
})Or use nuxt generate:
nuxt generateProgrammatic Prerendering
// nuxt.config.ts
export default defineNuxtConfig({
hooks: {
'prerender:routes'({ routes }) {
// Add dynamic routes
const posts = await fetchPostSlugs()
for (const slug of posts) {
routes.add(`/posts/${slug}`)
}
},
},
})Or in pages:
// server/api/posts.ts or a plugin
prerenderRoutes(['/posts/1', '/posts/2', '/posts/3'])Edge-Side Rendering
Render at CDN edge servers:
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages', // or 'vercel-edge', 'netlify-edge'
},
})Supported platforms:
- Cloudflare Pages/Workers
- Vercel Edge Functions
- Netlify Edge Functions
Conditional Rendering
Use import.meta.server and import.meta.client:
<script setup>
if (import.meta.server) {
// Server-only code
console.log('Running on server')
}
if (import.meta.client) {
// Client-only code
console.log('Running in browser')
}
</script>For components:
<template>
<ClientOnly>
<BrowserOnlyComponent />
<template #fallback>
<p>Loading...</p>
</template>
</ClientOnly>
</template><!-- Source references:
- https://nuxt.com/docs/guide/concepts/rendering
- https://nuxt.com/docs/getting-started/prerendering
- https://nuxt.com/docs/api/nuxt-config#routerules
-->
Related skills
Forks & variants (2)
Nuxt has 2 known copies in the catalog totaling 80 installs. They canonicalize to this original listing.
How it compares
Pick the nuxt lifecycle-hooks skill when hook-based extension in nuxt.config.ts or modules is needed instead of ad-hoc plugins that bypass documented Nuxt and Nitro event points.
FAQ
Where do Nuxt build-time hooks run?
Nuxt build-time hooks such as build:before and pages:extend run inside nuxt.config.ts or Nuxt modules via defineNuxtConfig hooks. Use them to intercept compilation and register custom routes before the app builds.
What does the nuxt skill cover beyond build hooks?
The nuxt skill also documents Nitro server runtime hooks and application lifecycle events alongside Nuxt build-time hooks. It was generated 2026-01-28 from antfu/skills sources/nuxt for extending full-stack behavior.
Is Nuxt safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.