GENERATION.md
# Generation Info - **Source:** `sources/nuxt` - **Git SHA:** `d67ad2c1cb23b5279464a2887cab82d94c8f5a04` - **Generated:** 2026-11-05
antfu/skills · GitHub
SSR, 자동 임포트 및 파일 기반 라우팅을 지원하는 Nuxt 풀스택 Vue 프레임워크입니다. Nuxt 앱, 서버 라우트, useFetch, 미들웨어 또는 하이브리드 렌더링을 다룰 때 사용하세요.
프로젝트 폴더에서 아래 명령어를 실행하고, 설치할 에이전트를 선택하세요.
npx skills add antfu/skills --skill nuxt설치 명령을 직접 실행해야 적용됩니다. 지원 에이전트와 필요한 권한·라이선스는 제작자의 안내를 확인하세요.
GENERATION.md# Generation Info - **Source:** `sources/nuxt` - **Git SHA:** `d67ad2c1cb23b5279464a2887cab82d94c8f5a04` - **Generated:** 2026-11-05
references/advanced-hooks.md---
name: lifecycle-hooks
description: Nuxt and Nitro hooks for extending build-time and runtime behavior
---
# 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
```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
```ts
// 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` | Add/modify routes (before meta scan) |
| `pages:resolved` | After page meta is scanned (with `scanPageMeta`) |
| `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 |
| `build:manifest` | Customize the manifest Nitro uses for `<script>`/`<link>` tags (entries have `prefetch`/`preload` flags) |
| `rsbuild:config` | Rsbuild builder: before the instance is created (config has `client` and `ssr` environments); `rspack:config`/`rspack:configResolved` also fire |
| `rsbuild:compile` / `rsbuild:compiled` | Before first compile / after an environment compiled |
## App Hooks (Runtime)
Used in plugins and composables:
### In Plugins
```ts
// 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
```ts
// composables/usePageTracking.ts
export function usePageTracking() {
const nuxtApp = useNuxtApp()
nuxtApp.hook('page:finish', () => {
trackPageView(useRoute().path)
})
}
```
## Server Hooks (Nitro)
Used in server plugins:
In Nitro v3 the plugin factory is `definePlugin` from `nitro`:
```ts
// server/plugins/hooks.ts
import { definePlugin } from 'nitro'
export default definePlugin((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>')
})
// Before request
nitroApp.hooks.hook('request', (event) => {
console.log('Request:', event.url.pathname)
})
// Single response hook (replaces beforeResponse/afterResponse in v3)
nitroApp.hooks.hook('response', (response, { event }) => {
console.log('Sending response')
})
})
```
> Inside a Nitro plugin, `useServerHooks()` from `nuxt/server` registers renderer hooks (`render:html`) and `NuxtServerHooks`; Nitro's own hooks (`request`, `response`, `error`) come from `useNitroHooks()` in `nitro/app`.
### Common Nitro Hooks
| Hook | When |
|------|------|
| `request` | Request received |
| `response` | Before/around sending response (replaces `beforeResponse`/`afterResponse`) |
| `render:html` | Before HTML is sent |
| `render:response` | Before response is finalized |
| `error` | Error occurred |
> **Nitro v3:** `beforeResponse`/`afterResponse` are merged into `response`. The compatibility layer re-emits them from `response` when active.
## Custom Hooks
### Define Custom Hook Types
```ts
// 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 'nitro/types' {
interface NitroRuntimeHooks {
'my-server:event': (data: any) => void
}
}
```
> **Nitro v3:** `nitropack` is renamed to `nitro` — augment `nitro/types` (not `nitropack/types`).
### Call Custom Hooks
```ts
// 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')
},
})
```
> **Nuxt 5:** `callHook` may now return `void` (not always a `Promise`), for performance. Always `await` it instead of chaining `.then()`/`.catch()`. Restore the always-Promise behavior with `experimental.asyncCallHook: true`.
## useRuntimeHook
Call hooks at runtime from components:
```vue
<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
```ts
// 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
```ts
// 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
```ts
// server/plugins/inject.ts
import { definePlugin } from 'nitro'
export default definePlugin((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
- https://nuxt.com/docs/getting-started/upgrade#migration-to-nitro-v3
-->
references/advanced-layers.md---
name: nuxt-layers
description: Extending Nuxt applications with layers for code sharing and reusability
---
# 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
```ts
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
'@my-org/base-layer',
'@nuxtjs/ui-layer',
],
})
```
### From Git Repository
```ts
// 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
```ts
// 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.ts
```
## Creating 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.json
```
### Layer nuxt.config.ts
```ts
// my-layer/nuxt.config.ts
export default defineNuxtConfig({
// Layer configuration
app: {
head: {
title: 'My Layer App',
},
},
// Shared modules
modules: ['@nuxt/ui'],
})
```
### Layer Components
```vue
<!-- my-layer/app/components/BaseButton.vue -->
<template>
<button class="base-btn">
<slot />
</button>
</template>
```
Use in consuming project:
```vue
<template>
<BaseButton>Click me</BaseButton>
</template>
```
### Layer Composables
```ts
// 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 priority
```
## Layer Aliases
Access layer files:
```ts
// Auto-scanned layers get aliases
import Component from '#layers/base/components/Component.vue'
```
Named aliases:
```ts
// my-layer/nuxt.config.ts
export default defineNuxtConfig({
$meta: {
name: 'my-layer',
},
})
```
```ts
// In consuming project
import { something } from '#layers/my-layer/utils'
```
## Publishing Layers
### As npm Package
```json
{
"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:
```bash
export GIGET_AUTH=<github-token>
```
## Layer Best Practices
### Use Resolved Paths
```ts
// 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
```ts
// nuxt.config.ts
export default defineNuxtConfig({
extends: [
['github:user/layer', { install: true }],
],
})
```
### Disable Layer Modules
```ts
// 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:
```bash
npx nuxi init --template layer my-layer
```
## Example: Theme Layer
```
theme-layer/
├── nuxt.config.ts
├── app/
│ ├── app.config.ts
│ ├── components/
│ │ ├── ThemeButton.vue
│ │ └── ThemeCard.vue
│ ├── composables/
│ │ └── useTheme.ts
│ └── assets/
│ └── theme.css
└── package.json
```
```ts
// theme-layer/nuxt.config.ts
export default defineNuxtConfig({
css: ['~/assets/theme.css'],
})
```
```ts
// theme-layer/app/app.config.ts
export default defineAppConfig({
theme: {
primaryColor: '#00dc82',
darkMode: false,
},
})
```
```ts
// 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
-->
references/advanced-module-authoring.md---
name: module-authoring
description: Complete guide to creating publishable Nuxt modules with best practices
---
# 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.json
```
## Module Definition
### Basic Module with Type-safe Options
```ts
// 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:
```ts
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
```ts
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
```ts
import { addImports, addImportsDir, addImportsSources, 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'))
// Auto-import from packages (Nuxt 4: accepts an array + package presets)
addImportsSources([
{ package: '@vueuse/core' }, // all of the package's documented exports
{ from: 'my-lib', imports: ['useThing', 'useOther'] },
])
},
})
```
### Keyed Composables (SSR/client key injection)
If your composable needs a stable key shared between server and client (like `useState`/`useAsyncData`), register it so Nuxt's compiler injects a unique key when called with fewer than `argumentLength` args:
```ts
export default defineNuxtModule({
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
nuxt.options.optimization.keyedComposables.push({
name: 'useMyState',
source: resolve('./runtime/composables/state'), // exact source file (no barrels)
argumentLength: 2,
})
},
})
```
The call must be statically analyzable and imported directly from `source` (barrel re-exports, dynamic/indirect calls, and reassignments are not transformed).
### Plugins
```ts
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
```ts
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
```ts
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
```ts
import { addServerPlugin, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
const { resolve } = createResolver(import.meta.url)
addServerPlugin(resolve('./runtime/server/plugin'))
},
})
```
```ts
// runtime/server/plugin.ts (Nitro v3)
import { definePlugin } from 'nitro'
export default definePlugin((nitroApp) => {
nitroApp.hooks.hook('request', (event) => {
console.log('Request:', event.url.pathname)
})
})
```
## Nuxt 5 / Nitro v3 for Module Authors
- **Portable server code:** import from **`nuxt/server`** (ships in Nuxt 4.6+) so one file runs under `nitropack` v2, Nitro v3, and other builders with no h3/Nitro peer dependency. Leave `nuxt/server` external when bundling.
- **Type augmentation moved:** `nitropack/types` → `nitro/types`.
- **Route rule types** are declared on `h3/rules` now (`RouteRuleConfig` for config, `RouteRules` for runtime), not `NitroRouteConfig`/`NitroRouteRules`:
```ts
declare module 'h3/rules' {
interface RouteRuleConfig { myModule?: { enabled: boolean } }
interface RouteRules { myModule?: { enabled: boolean } }
}
```
- **Compatibility layer:** a module whose server files still import `h3`/`nitropack`/`#internal/nitro`/`#imports` keeps running via an automatic Nitro v2 compatibility layer (`NUXT_B9003` warning names it). To support Nuxt <4.6 and Nitro v3 at once, register both handler files:
```ts
addServerHandler({
route: '/api/my-module/status',
handler: {
nuxt: resolve('./runtime/server/status'), // nuxt/server only
nitro2: resolve('./runtime/server/status.legacy'), // h3 / nitropack
},
})
```
- **Dev server handlers:** `addDevServerHandler` takes the same variant keys (`handler: { nuxt, nitro2, nitro3 }`), plus `method`, `middleware`, `meta`. They run in the Nuxt process, so write `nuxt` as `event => fn(event.req)` against web `Request`/`Response` and never import `nuxt/server` there; `nitro2` uses h3 v1 with `toWebRequest(event)` (add `h3@^1` to dependencies). The object form needs `@nuxt/kit@^4.6` in `dependencies`. Routes: `nuxt`/`nitro3` match exactly, `nitro2` matches as a prefix; end the route in `/**` to match subpaths on all (`@nuxt/kit@^4.6.1`). A `nitro2` handler sees `event.path` relative to its route base.
- **Other variants:** `resolveServerVariant({ nuxt, nitro2 })` resolves the right file for aliases/type templates (or `undefined`); `addServerImports({ from: { nuxt, nitro2 } })` accepts variants. `nitro3` files may import `nitro/*` alone or alongside `nuxt/server`.
- **Detection:** Nuxt infers the API from imports of registered files and their imports (`import type` ignored); override with `meta.compatibility.server`. Undetectable code is treated as `nitro2`. With `nitropack` v2 or `nitro` v3 hosts the `nitro2`/`nitro3` variant is preferred over `nuxt`; to test the `nuxt` variant set `server.builder` to a non-Nitro builder (e.g. `@nuxt/vite-server`).
- **Server utilities** taking the event should type it as `RequestEvent` from `nuxt/server` (or `Pick<RequestEvent, 'req' | 'context'>`), not `H3Event`. Type module runtime config by augmenting `RuntimeConfig`/`PublicRuntimeConfig` from `nuxt/schema`.
- **Plugins, storage, caching, tasks** have no `nuxt` variant: register as `nitro2` now, add `nitro3` when porting (`useKV` from `nitro/kv`, `defineTask` from `nitro/task`). In Nitro v3, `globalThis.$fetch` is gone: use `serverFetch(event, path)` from `nuxt/server`.
- Declare all real dependencies in `package.json` — Nitro v3 no longer hoists `nitropack` v2's own deps into the server bundle.
## Templates and Virtual Files
### Generate Virtual Files
```ts
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',
dependsOn: [], // optional: skip dev regeneration on file edits; only if output doesn't read watched file contents
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
```ts
// 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
```ts
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:
```ts
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
```ts
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`:
```ts
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
```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,
}
// Or use kit utilities: extendViteConfig, extendWebpackConfig, extendRsbuildConfig(config => ...)
// Plugins: addVitePlugin, addWebpackPlugin, addRsbuildPlugin, or addBuildPlugin({ vite, webpack, rsbuild, rspack })
// addVitePlugin targets the app environments (client/ssr), never `nitro`; for the nitro Vite
// environment push to nuxt.options.vite.plugins with applyToEnvironment: e => e.name === 'nitro'
// 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
```ts
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
```ts
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
```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": "^4.0.0"
},
"devDependencies": {
"@nuxt/module-builder": "latest",
"nuxt": "^4.0.0"
}
}
```
## Disabling Modules
Users can disable a module via config key:
```ts
// 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 `ModuleOptions` interface
- Use `moduleDependencies` instead of `installModule`
- 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
- https://nuxt.com/docs/guide/modules/recipes-basics
- https://nuxt.com/docs/guide/modules/module-dependencies
- https://nuxt.com/docs/guide/modules/server-compatibility
- https://nuxt.com/docs/guide/modules/recipes-advanced
- https://nuxt.com/docs/api/kit/builder
-->
references/best-practices-data-fetching.md---
name: data-fetching-best-practices
description: Patterns and best practices for efficient data fetching in Nuxt
---
# 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` |
| Reusable API client with shared defaults | `createUseFetch` factory |
## Await vs Non-Await Usage
The `await` keyword controls whether data fetching **blocks navigation**:
### With `await` - Blocking Navigation
```vue
<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)
```vue
<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`:
```vue
<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
```vue
<script setup lang="ts">
// This fetches TWICE: once on server, once on client
const data = await $fetch('/api/posts')
</script>
```
### ✅ Correct: Use useFetch
```vue
<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
```vue
<script setup lang="ts">
// Key is auto-generated from file/line - can cause issues
const { data } = await useAsyncData(() => fetchPosts())
</script>
```
### ✅ Better: Explicit Keys
```vue
<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
```vue
<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
```vue
<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
```vue
<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
```vue
<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
```vue
<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
```vue
<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
```vue
<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
```vue
<script setup lang="ts">
const { data, refresh, status } = await useFetch('/api/posts')
async function refreshPosts() {
await refresh()
}
</script>
```
### Conditional Fetching
```vue
<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
```vue
<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
```vue
<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
```vue
<!-- 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>
```
## Centralize API Config with createUseFetch
Instead of hand-rolling a wrapper around `useFetch` (and worrying about whether to `await` it), use the `createUseFetch` factory. It produces a fully typed composable with shared `baseURL`, headers, and interceptors:
```ts
// app/composables/useAPI.ts
export const useAPI = createUseFetch({
baseURL: 'https://api.nuxt.com',
onRequest({ options }) {
const { session } = useUserSession()
if (session.value?.token) {
options.headers.set('Authorization', `Bearer ${session.value.token}`)
}
},
async onResponseError({ response }) {
if (response.status === 401) await navigateTo('/login')
},
})
```
```vue
<script setup lang="ts">
const { data: profile } = await useAPI('/me') // auth + 401 handling baked in
</script>
```
To add reusable behavior (polling, refresh on focus, extra options or return values), pass `addons` built with `defineUseFetchAddon` / `defineUseAsyncDataAddon` to the factory (v4.6+); see core-data-fetching.
For lower-level control, create a custom `$fetch` instance in a plugin and wrap it with `useAsyncData` (or pass it to `createUseFetch`) — this avoids double fetching during SSR:
```ts
// app/plugins/api.ts
export default defineNuxtPlugin(() => {
const api = $fetch.create({ baseURL: 'https://api.nuxt.com' })
return { provide: { api } }
})
```
```vue
<script setup lang="ts">
const { $api } = useNuxtApp()
const { data } = await useAsyncData('modules', () => $api('/modules'))
</script>
```
## Avoid useAsyncData for Side Effects
### ❌ Wrong: Side Effects in useAsyncData
```vue
<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
```vue
<script setup lang="ts">
await callOnce(async () => {
await store.fetchUser()
})
</script>
```
<!--
Source references:
- https://nuxt.com/docs/4.x/getting-started/data-fetching
- https://nuxt.com/docs/4.x/api/composables/use-fetch
- https://nuxt.com/docs/4.x/api/composables/use-async-data
- https://nuxt.com/docs/4.x/api/composables/use-lazy-fetch
- https://nuxt.com/docs/4.x/guide/recipes/custom-usefetch
-->
references/best-practices-ssr.md---
name: ssr-best-practices
description: Avoiding SSR context leaks, hydration mismatches, and proper composable usage
---
# 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
```ts
// composables/bad.ts
// Called at module level - no Nuxt context!
const config = useRuntimeConfig()
export function useMyComposable() {
return config.public.apiBase
}
```
### ✅ Correct: Composable Inside Function
```ts
// 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>` blocks
- `setup()` function
- `defineNuxtPlugin()` callbacks
- `defineNuxtRouteMiddleware()` callbacks
```ts
// ✅ 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
```ts
// 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
```ts
// 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
```vue
<script setup>
// localStorage doesn't exist on server!
const theme = localStorage.getItem('theme') || 'light'
</script>
```
### ✅ Correct: Use SSR-safe Alternatives
```vue
<script setup>
// useCookie works on both server and client
const theme = useCookie('theme', { default: () => 'light' })
</script>
```
### ❌ Wrong: Random/Time-based Values
```vue
<template>
<div>{{ Math.random() }}</div>
<div>{{ new Date().toLocaleTimeString() }}</div>
</template>
```
### ✅ Correct: Use useState for Consistency
```vue
<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
```vue
<template>
<!-- window doesn't exist on server -->
<div v-if="window?.innerWidth > 768">Desktop</div>
</template>
```
### ✅ Correct: Use CSS or ClientOnly
```vue
<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>
```
### iOS Safari Phone Numbers
iOS Safari auto-links phone numbers, dates, addresses and emails before hydration, causing iOS-only mismatches. Disable with `app.head.meta: [{ name: 'format-detection', content: 'telephone=no, date=no, email=no, address=no' }]`, and wrap numbers that should stay tappable in `<a href="tel:...">`.
## Browser-only Code
### Use `import.meta.client`
```vue
<script setup>
if (import.meta.client) {
// Only runs in browser
window.addEventListener('scroll', handleScroll)
}
</script>
```
### Use `onMounted` for DOM Access
```vue
<script setup>
const el = ref<HTMLElement>()
onMounted(() => {
// Safe - only runs on client after hydration
el.value?.focus()
initThirdPartyLib()
})
</script>
```
### Dynamic Imports for Browser Libraries
```vue
<script setup>
onMounted(async () => {
const { Chart } = await import('chart.js')
new Chart(canvas.value, config)
})
</script>
```
## Server-only Code
### Use `import.meta.server`
```vue
<script setup>
if (import.meta.server) {
// Only runs on server
const secrets = useRuntimeConfig().apiSecret
}
</script>
```
### Server Components
```vue
<!-- 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
```vue
<script setup>
await someAsyncOperation()
const route = useRoute() // May fail - context lost after await
</script>
```
### ✅ Correct: Get Context First
```vue
<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
```ts
// plugins/analytics.client.ts
export default defineNuxtPlugin(() => {
// Only runs on client
initAnalytics()
})
```
### Server-only Plugins
```ts
// plugins/server-init.server.ts
export default defineNuxtPlugin(() => {
// Only runs on server
initServerConnections()
})
```
### Provide/Inject Pattern
```ts
// plugins/api.ts
export default defineNuxtPlugin(() => {
const api = createApiClient()
return {
provide: {
api,
},
}
})
```
```vue
<script setup>
const { $api } = useNuxtApp()
const data = await $api.get('/users')
</script>
```
## Third-party Library Integration
### ❌ Wrong: Import at Top Level
```vue
<script setup>
import SomeLibrary from 'browser-only-lib' // Breaks SSR
</script>
```
### ✅ Correct: Dynamic Import
```vue
<script setup>
let library: typeof import('browser-only-lib')
onMounted(async () => {
library = await import('browser-only-lib')
library.init()
})
</script>
```
### Use ClientOnly Component
```vue
<template>
<ClientOnly>
<BrowserOnlyComponent />
<template #fallback>
<div class="skeleton">Loading...</div>
</template>
</ClientOnly>
</template>
```
## Resource Hints and Deferred Work
Nuxt emits `preload`/`modulepreload` for chunks the page needs and `prefetch` for lazily imported chunks, rendered as `data-href` and activated after `onNuxtReady` (`experimental.deferPrefetchHints: false` to prefetch immediately). To stop a chunk group being prefetched (e.g. signed-in-app chunks on a public prerendered page), set `chunk.prefetch = false` per entry in the `build:manifest` hook. Defer non-critical client work with `onNuxtReady(() => import('big-lib'), { timeout: 2000 })`.
## Debugging SSR Issues
### Check Rendering Context
```vue
<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/guide/best-practices/performance
- https://nuxt.com/docs/getting-started/state-management#best-practices
-->
references/core-cli.md--- name: cli-commands description: Nuxt CLI commands for development, building, and project management --- # CLI Commands Nuxt provides CLI commands via `nuxi` (or `npx nuxt`) for development, building, and project management. > The CLI reference now lives in the [`nuxt/cli`](https://github.com/nuxt/cli) repo (its docs moved out of the main Nuxt repo). Commands and flags are unchanged. ## Project Initialization ### Create New Project ```bash # 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-modules ``` **Options:** | 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 ```bash # 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 --tunnel ``` **Options:** | 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_PORT` or `PORT` - Default port - `NUXT_HOST` or `HOST` - Default host ## Building ### Production Build ```bash # 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 staging ``` Output is created in `.output/` directory. ### Static Generation ```bash # Generate static site (prerenders all routes) npx nuxt generate ``` Equivalent to `nuxt build --prerender`. Creates static HTML files for deployment to static hosting. Configure prerendering with the top-level `prerender` option (`routes`, `ignore`, `crawlLinks`), which works with any server builder; Nitro-only options stay in `nitro.prerender`. The old `nitro.prerender.routes`/`crawlLinks` examples are superseded. ### Preview Production Build ```bash # Preview after build npx nuxt preview # Custom port npx nuxt preview --port 4000 ``` ## Utilities ### Prepare (Type Generation) ```bash # Generate TypeScript types and .nuxt directory npx nuxt prepare ``` Run after cloning or when types are missing. ### Type Check ```bash # Run TypeScript type checking npx nuxt typecheck ``` ### Analyze Bundle ```bash # Analyze production bundle npx nuxt analyze ``` Opens visual bundle analyzer. ### Cleanup ```bash # Remove generated files (.nuxt, .output, node_modules/.cache) npx nuxt cleanup ``` ### Info ```bash # Show environment info (useful for bug reports) npx nuxt info ``` ### Upgrade ```bash # Upgrade Nuxt to latest version npx nuxt upgrade # Upgrade to nightly release npx nuxt upgrade --nightly ``` ## Module Commands ### Add Module ```bash # Add a Nuxt module npx nuxt module add @nuxt/ui npx nuxt module add @nuxt/image ``` Installs and adds to `nuxt.config.ts`. ### Build Module (for module authors) ```bash # Build a Nuxt module npx nuxt build-module ``` ## DevTools ```bash # Enable DevTools globally npx nuxt devtools enable # Disable DevTools npx nuxt devtools disable ``` ## Common Workflows ### Development ```bash # Install dependencies and start dev pnpm install pnpm dev # or npx nuxt dev ``` ### Production Deployment ```bash # Build and preview locally pnpm build pnpm preview # Or for static hosting pnpm generate ``` ### After Cloning ```bash # Install deps and prepare types pnpm install npx nuxt prepare ``` ## Environment-specific Builds ```bash # Development build npx nuxt build --envName development # Staging build npx nuxt build --envName staging # Production build (default) npx nuxt build --envName production ``` Corresponds to `$development`, `$env.staging`, `$production` in `nuxt.config.ts`. ## Layer Extension ```bash # Dev with additional layer npx nuxt dev --extends ./base-layer # Build with layer npx nuxt build --extends ./base-layer ``` <!-- Source references: - https://github.com/nuxt/cli - https://nuxt.com/docs/api/commands -->
references/core-config.md---
name: configuration
description: Nuxt configuration files including nuxt.config.ts, app.config.ts, and runtime configuration
---
# 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:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
// Configuration options
devtools: { enabled: true },
modules: ['@nuxt/ui'],
})
```
### Path Aliases
The default `srcDir` is `app/`, so path aliases resolve as:
| Alias | Resolves to |
|-------|-------------|
| `~` / `@` | `<rootDir>/app` |
| `~~` / `@@` | `<rootDir>` (project root) |
| `#shared` | `<rootDir>/shared` |
| `#server` | `<rootDir>/server` |
Reference root-level paths (modules, server handlers) with `~~` or `#server`:
```ts
export default defineNuxtConfig({
modules: ['~~/custom-modules/awesome.js'], // relative to rootDir
serverHandlers: [
{ route: '/foo/**', handler: '#server/foohandler.ts' },
],
})
```
### `nuxt.config` Loading (Nuxt 5: no more `jiti`)
Nuxt 5 no longer bundles `jiti`. `nuxt.config.ts`, files in `modules/`, and layer configs are loaded by the Node runtime directly (requires Node `22.19`/`24.11`+, which strip types natively). Two consequences:
- **Relative imports in these files need an explicit extension** — write `./build/plugin.ts`, not `./build/plugin` (TS reports `TS2835`). Bare package imports are unaffected.
- **Use erasable TypeScript only** in config/module files — no `enum`, `namespace`, constructor parameter properties, or decorators (TS reports `TS1294`).
App and `shared/` code go through Vite and are unaffected. Published layers/modules must ship compiled JS (a shipped config as `nuxt.config.mjs`). Install `jiti` (`-D`) to restore the old fallback behavior; a `nuxt.schema` file always needs it.
### Compatibility Version
Nuxt 5 behavior is now the default. `future.compatibilityVersion` is mostly historical — Nuxt 4 code paths that were opt-in are now unconditional. Individual behaviors are restored via specific `experimental`/config options (see each section below and the [upgrade notes](#nuxt-5-default-changes)).
### Environment Overrides
Configure environment-specific settings:
```ts
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:
```ts
// 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:
```ini
# .env
NUXT_API_SECRET=api_secret_token
NUXT_PUBLIC_API_BASE=https://api.example.com
```
Access in components/composables:
```vue
<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):
```ts
// app/app.config.ts
export default defineAppConfig({
title: 'Hello Nuxt',
theme: {
dark: true,
colors: {
primary: '#ff0000',
},
},
})
```
Access in components:
```vue
<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:
```ts
export default defineNuxtConfig({
// Nitro configuration
nitro: {
// nitro options (storage mounts are `nitro.kv` + `useKV()`; `nitro.storage`/`useStorage()` are deprecated)
},
// Prerendering applies to any server builder; Nitro-only options (concurrency, failOnError) stay in `nitro.prerender`
prerender: {
routes: ['/user/1', '/user/2'],
ignore: ['/dynamic'],
crawlLinks: true,
},
// Client bundler: 'vite' (default) | 'webpack' | 'rsbuild' (`rspack` is deprecated, use rsbuild + `rsbuild: {}` options)
// builder: 'rsbuild',
// Vite configuration
vite: {
// vite options
vue: {
// @vitejs/plugin-vue options
},
},
// PostCSS configuration
postcss: {
// postcss options
},
})
```
### Environment-specific Vite Config
Top-level `vite` options are shared. Use `$client` and `$server` to target a single Vite build:
```ts
export default defineNuxtConfig({
vite: {
$client: {
build: { rollupOptions: { output: { manualChunks: { analytics: ['analytics-package'] } } } },
},
$server: {
build: { sourcemap: 'inline' }, // `sourcemap` also accepts 'hidden' | 'nosource' at the top level
},
},
})
```
Vite 8 / Rolldown: `vite.esbuild` and `vite.optimizeDeps.esbuildOptions` are deprecated for `vite.oxc` and `vite.optimizeDeps.rolldownOptions`; `build.rollupOptions` for `build.rolldownOptions`. A Nitro rollup plugin that must run before Vite's resolution needs to be a Vite plugin with `enforce: 'pre'` (scope server-only plugins with `applyToEnvironment: env => env.name === 'nitro'`).
## Vue Configuration
Enable Vue experimental features:
```ts
export default defineNuxtConfig({
vue: {
propsDestructure: true,
},
})
```
## Nuxt 5 Default Changes
Many former experimental flags are now on by default. Each can be reverted via the named option:
| Behavior | Now default | Restore Nuxt 4 with |
|----------|-------------|---------------------|
| Case-sensitive page routing | `router.options.sensitive: true` | `router.options.sensitive: false` |
| Typed pages (`useRoute`/`navigateTo`/`<NuxtLink>` type-checked) | on | `experimental.typedPages: false` |
| Page component name = route name | on | `experimental.normalizePageNames: false` |
| All serializable `definePageMeta` written to route record | on | `experimental.extractSerializablePageMeta: false` |
| Payload inlined in HTML, `_payload.json` only for SPA nav | `experimental.payloadExtraction: 'client'` | `payloadExtraction: true` |
| `clearNuxtState` resets to `useState` init value | on | `experimental.defaults.useState.resetOnClear: false` |
| `await navigateTo()` early-returns from `setup()` | on | `experimental.navigateToEarlyReturn: false` |
| `callHook` may return `void` (not always a Promise) | on | `experimental.asyncCallHook: true` |
| Client-only SSR placeholder is a comment node, not `<div>` | on | `experimental.clientNodePlaceholder: false` |
| Reuse builder's file watcher | `experimental.watcher: 'builder'` | `watcher: 'chokidar'` |
| Server auto-imports (Nitro helpers) | off | `experimental.nitroAutoImports: true` |
| Error format: page errors return the HTML error page, server route errors return JSON (request headers no longer matter) | `experimental.inlineErrorRendering` on | `experimental.inlineErrorRendering: false` |
| `$fetch` is not set on `globalThis` in the browser (import from `#build/fetch`) | on | provide your own instance via a plugin (`provide: { api: $fetch.create(...) }`) |
| `routeRules` keys use URLPattern: `*` matches nested paths, one `*`/`**` per key, regex constraints cannot match encoded chars | on | use named params (`/blog/:slug`) |
| `server/utils/` subdirectories not auto-imported (warns `NUXT_B9006`) | on | re-export from a top-level file, or `nitroLegacy.imports` |
| h3 v1 / `nitropack` imports in your `server/` dir | not supported (`NUXT_B9005` lists them) | `nitroLegacy: true` (or per-switch object) while migrating |
| `error.data` parsed (never stringified) | forced on | — (`parseErrorData` no longer configurable) |
| Vue Options API compiled out of client bundle | on | `vue: { optionsApi: true }` |
| `noUncheckedSideEffectImports` in generated tsconfig | on | `typescript.tsConfig.compilerOptions.noUncheckedSideEffectImports: false` |
**Removed options** (behavior now unconditional): `experimental.viteEnvironmentApi`, `experimental.routeTypedFetch`, `experimental.renderJsonPayloads`, `experimental.externalVue`. Delete them from config.
`unhead.legacy` is ignored (setting it warns); Capo title sorting is always on.
## Other Experimental Flags
On by default (disable with `false`): `serverPathFallback` (client navigation to a path matching no page, e.g. `<NuxtLink to="/rss.xml">`, loads as a full document so public files/server routes respond), `templateDiagnostics` (build warnings `NUXT_B4025` for `<NuxtLink to>` matching no route, `B3014` `v-on`/`v-model` on server components, `B3015` function/class props to server components or `<NuxtIsland>`), `deferPrefetchHints` (prefetch hints rendered as `data-href`, activated by `onNuxtReady`), `compileHints` (V8 compile hint on entry/layout/page chunks, Vite prod builds), `compilerComponentResolution` (auto-imported components resolved in the Vue compiler).
Notable opt-in flags under `experimental` (see source for the full list):
```ts
export default defineNuxtConfig({
experimental: {
// Stream the HTML shell first, then render body progressively (better TTFB)
ssrStreaming: true,
// Reject fetches to paths no server route answers (true | 'isomorphic')
strictRouteTypes: true,
// Respond 404 for unmatched paths without loading the Vue app/plugins/middleware
early404: true,
// Prerender error.vue into 404.html at build time
prerenderErrorPages: true,
// Default options for core composables/components
defaults: {
nuxtLink: { prefetch: true, prefetchOn: { visibility: true } },
useAsyncData: { deep: true },
},
},
})
```
<!--
Source references:
- https://nuxt.com/docs/getting-started/configuration
- https://nuxt.com/docs/getting-started/upgrade
- https://nuxt.com/docs/guide/going-further/runtime-config
- https://nuxt.com/docs/api/nuxt-config
- https://nuxt.com/docs/guide/going-further/experimental-features
- https://nuxt.com/docs/getting-started/prerendering
- https://nuxt.com/docs/errors/b4025
- https://nuxt.com/docs/errors/b3014
- https://nuxt.com/docs/errors/b3015
-->
references/core-data-fetching.md---
name: data-fetching
description: useFetch, useAsyncData, and $fetch for SSR-friendly data fetching
---
# 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
- `createUseFetch` / `createUseAsyncData` - factories (support `addons`) to build typed custom composables with baked-in defaults
### On `await`
`await` does **not** change the server-rendered HTML — SSR always waits for the request and serializes the populated result. What it changes is client behavior:
- **With `await`** (default): setup pauses until data is ready; client-side navigation is blocked until it resolves (user stays put, optionally with `<NuxtLoadingIndicator>`).
- **Without `await`**: setup continues immediately, `data` starts at its default and fills in later; client navigation happens instantly and you handle `status`/`error` yourself.
Prefer the explicit `lazy` option (or `useLazyFetch`/`useLazyAsyncData`) for non-blocking intent. Note: `await`-ing a `lazy` call does **not** block client navigation — drop `lazy` if you want the wait.
## useFetch
Primary composable for fetching data in components:
```vue
<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
```ts
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
```vue
<script setup lang="ts">
const page = ref(1)
const { data } = await useFetch('/api/posts', {
query: { page }, // Automatically refetches when page changes
})
</script>
```
### Computed URL
```vue
<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:
```vue
<script setup lang="ts">
const { data, error } = await useAsyncData('user', () => {
return myCustomFetch('/user/profile')
})
</script>
```
### Multiple Requests
```vue
<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>
```
## Custom Fetchers: createUseFetch / createUseAsyncData
Factory macros that produce a fully typed custom composable with pre-defined options. Must be an **exported** declaration inside `app/composables/` (Nuxt injects dedup keys at build time).
```ts
// app/composables/useAPI.ts
export const useAPI = createUseFetch({
baseURL: 'https://api.nuxt.com',
// shared interceptors, headers, etc.
onResponseError({ response }) {
if (response.status === 401) navigateTo('/login')
},
})
```
```vue
<script setup lang="ts">
// Same signature/return as useFetch, with defaults applied
const { data } = await useAPI('/modules')
// Caller can still override any option
const { data: other } = await useAPI('/modules', { baseURL: 'https://other.com' })
</script>
```
**Default vs Override mode:**
```ts
// Plain object → options act as DEFAULTS (caller can override)
export const useAPI = createUseFetch({ baseURL: '/api', lazy: true })
// Function → options OVERRIDE caller's (enforce auth/baseURL)
export const useAPI = createUseFetch(callerOptions => ({
baseURL: 'https://api.nuxt.com', // always enforced
}))
```
Use the **function form** when you need `useNuxtApp()` (called in setup context, not module scope):
```ts
// app/composables/useAPI.ts
export const useAPI = createUseFetch(callerOptions => ({
$fetch: useNuxtApp().$api as typeof $fetch,
...callerOptions,
}))
```
`createUseAsyncData` works identically for wrapping arbitrary async functions:
```ts
// app/composables/useCachedData.ts
export const useCachedData = createUseAsyncData({
getCachedData(key, nuxtApp) {
return nuxtApp.payload.data[key] ?? nuxtApp.static.data[key]
},
})
```
### Addons
Both factories accept `addons: [...]` built with `defineUseFetchAddon` / `defineUseAsyncDataAddon` (v4.6+). `setup(options)` runs before the call (declare custom typed call-site options via `UseFetchAddonOptions<{ ... }>` / `UseAsyncDataAddonOptions<{ ... }>`, may push to `options.onRequest`), and may return `(asyncData) => ext | void` that runs after, to attach watchers/cleanup and extend the returned object. Addons run in array order (first is the outermost middleware); duplicates run once.
```ts
const polling = defineUseAsyncDataAddon({
setup: (options: UseAsyncDataAddonOptions<{ pollEvery?: number }>) => {
if (import.meta.server || !options.pollEvery) { return }
return (asyncData) => {
const id = setInterval(() => asyncData.refresh(), options.pollEvery)
onScopeDispose(() => clearInterval(id))
}
},
})
export const usePolledAsyncData = createUseAsyncData({ addons: [polling] })
// await usePolledAsyncData('key', () => $fetch('/api/x'), { pollEvery: 30_000 })
```
`useAsyncData`/`useFetch` also take a `middleware: AsyncDataMiddleware[]` option (v4.6+): `(next, { signal }) => Promise` functions wrapping the handler; must be consistent across calls sharing a key.
Related: `onNuxtReady(cb, { timeout, signal })` runs a client-only callback when the app is ready and the browser idle (`timeout` caps the idle wait, `signal` skips it if aborted).
> Replaces the old "don't await your custom `useFetch` wrapper" caveat — use these factories instead of hand-rolled wrappers.
## Typed Routes (Nuxt 5)
`$fetch` and `useFetch` are typed from the routes your server builder reports (not nitro's `InternalApi`). The response is typed from the matching handler's return type, keyed by HTTP method:
```ts
// server/api/todos.get.ts
export default defineEventHandler(() => [{ id: '1', title: 'Buy milk' }])
```
```ts
const todos = await $fetch('/api/todos')
// ^? { id: string, title: string }[]
```
If a handler validates its request, the `body`/`query`/`headers` are enforced on the call, and a method the route doesn't handle is rejected:
```ts
await $fetch('/api/todos', { method: 'POST', body: { title: 42 } })
// ^ not assignable to string
```
Key points:
- The **`params` option is removed** from `$fetch`/`useFetch` (it was an alias for `query`) — move it to `query`.
- A `baseURL` is resolved into the path before matching, so passing an already-prefixed path is an error.
- A runtime path, absolute URL, or `Request` object resolves to `unknown`; provide an explicit type: `$fetch<{ id: string }>(\`/api/${x}\`)`.
- A path Nuxt doesn't recognize is `unknown` by default; set `experimental.strictRouteTypes: true` (or `'isomorphic'`) to make it an error.
- Declare routes Nuxt can't see (runtime-registered, external APIs) by augmenting `ServerRoutes`:
```ts
// shared/server-routes.d.ts
import type { Endpoint } from 'nuxt/app'
declare module '@nuxt/schema' {
interface ServerRoutes {
'/api/hello': {
[Endpoint]: { GET: { response: { message: string } } }
}
}
}
```
## $fetch
For client-side events (form submissions, button clicks):
```vue
<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 (`undefined` until resolved) |
| `error` | `Ref<Error>` | Error if request failed |
| `status` | `Ref<'idle' \| 'pending' \| 'success' \| 'error'>` | Request status |
| `pending` | `Ref<boolean>` | Whether a request is in progress |
| `refresh` | `() => Promise` | Refetch data |
| `execute` | `() => Promise` | Alias for refresh |
| `clear` | `() => void` | Reset to default/idle and cancel pending requests |
> Prefer `status` over `pending` for fine-grained state. `useFetch` no longer accepts a top-level `timeout` option (still available on `useAsyncData`); use a `cache` option (`'default'`, `'no-store'`, `false`, etc.) for Fetch cache control.
## Lazy Fetching
Don't block navigation:
```vue
<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
```vue
<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
`useFetch` generates its key from the URL **plus** the fetch options and the call's source location — so two `useFetch('/api/user')` calls in different components have **different** keys and each run their own request. To share data, pass the **same explicit `key`**:
```vue
<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:
```ts
// Refresh specific key
await refreshNuxtData('current-user')
// Refresh all data
await refreshNuxtData()
// Clear cached data
clearNuxtData('current-user')
```
## Interceptors
```ts
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`:
```vue
<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
- https://nuxt.com/docs/api/composables/create-use-fetch
- https://nuxt.com/docs/api/composables/create-use-async-data
- https://nuxt.com/docs/guide/recipes/custom-usefetch
- https://nuxt.com/docs/api/utils/define-use-fetch-addon
- https://nuxt.com/docs/api/utils/define-use-async-data-addon
- https://nuxt.com/docs/api/utils/on-nuxt-ready
-->
references/core-deployment.md---
name: deployment
description: Deploying Nuxt applications to various hosting platforms
---
# Deployment
Nuxt is platform-agnostic thanks to [Nitro](https://nitro.build), 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
```bash
# Build for Node.js
nuxt build
# Run production server
node .output/server/index.mjs
```
Environment variables:
- `PORT` or `NITRO_PORT` (default: 3000)
- `HOST` or `NITRO_HOST` (default: 0.0.0.0)
### Static Generation
```bash
# Generate static site
nuxt generate
```
Output in `.output/public/` - deploy to any static host.
### Prerender Configuration
Use the top-level `prerender` option (`routes`, `ignore`, `crawlLinks`) rather than `nitro.prerender`; Nitro-specific options (`concurrency`, `failOnError`) remain in `nitro.prerender`.
```ts
export default defineNuxtConfig({
prerender: { crawlLinks: true, routes: ['/sitemap.xml'], ignore: ['/dynamic'] },
})
```
### Preset Configuration
```ts
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'vercel', // or 'netlify', 'cloudflare-pages', etc.
},
})
```
Or via environment variable:
```bash
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
```bash
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
```
**Pros:**
- 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
```bash
# Install Netlify CLI
npm i -g netlify-cli
# Deploy
netlify deploy --prod
```
**Pros:**
- 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
```bash
# Build with Cloudflare preset
NITRO_PRESET=cloudflare-pages nuxt build
```
**Pros:**
- 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
```yaml
# .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
```dockerfile
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"]
```
```bash
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
- https://nuxt.com/docs/getting-started/prerendering
-->
references/core-directory-structure.md---
name: directory-structure
description: Nuxt project folder structure, conventions, and file organization
---
# Directory Structure
Nuxt uses a conventions-based directory structure. Understanding it is key to effective development.
> **Directory layout:** The default `srcDir` is `app/`. All Vue application code (`app.vue`, `components/`, `composables/`, `pages/`, etc.) lives inside `app/`, while `server/`, `shared/`, `public/`, `modules/`, `layers/` and `nuxt.config.ts` stay at the project root.
## Standard Project Structure
```
my-nuxt-app/
├── app/ # srcDir — all Vue app code (default in Nuxt 4)
│ ├── app.vue # Root component
│ ├── app.config.ts # App configuration (runtime)
│ ├── error.vue # Error page
│ ├── assets/ # Build-processed assets (CSS, images)
│ ├── components/ # Auto-imported Vue components
│ ├── composables/ # Auto-imported composables
│ ├── layouts/ # Layout components
│ ├── middleware/ # Route middleware
│ ├── pages/ # File-based routing
│ ├── plugins/ # Nuxt plugins
│ └── utils/ # Auto-imported utilities
├── server/ # Server-side code (root level)
│ ├── api/ # API routes (/api/*)
│ ├── routes/ # Server routes
│ ├── middleware/ # Server middleware
│ ├── plugins/ # Nitro plugins
│ └── utils/ # Server utilities (auto-imported)
├── shared/ # Code shared between app and server
│ ├── utils/ # Auto-imported in both app and server
│ └── types/ # Auto-imported types
├── public/ # Static assets (served as-is)
├── content/ # Content files (@nuxt/content)
├── layers/ # Local layers (auto-scanned)
├── modules/ # Local modules
├── nuxt.config.ts # Nuxt configuration
├── package.json
└── tsconfig.json
```
## Key Directories
### `app/` Directory
The `app/` directory is the default `srcDir` in Nuxt 4 and holds all Vue application code. Customize it if needed:
```ts
// nuxt.config.ts - customize source directory
export default defineNuxtConfig({
srcDir: 'src/', // Use 'src/' instead of the default 'app/'
})
```
**Aliases** (Nuxt 4 defaults):
| Alias | Resolves to |
|-------|-------------|
| `~` / `@` | `<rootDir>/app` (the srcDir) |
| `~~` / `@@` | `<rootDir>` (project root) |
| `#shared` | `<rootDir>/shared` |
| `#server` | `<rootDir>/server` |
Because `~` now points at `app/`, reference root-level files (modules, server handlers) with `~~` or the dedicated aliases — e.g. `~~/server/handler.ts` or `#server/handler.ts`.
### `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:
```vue
<template>
<LazyHeavyChart v-if="showChart" />
</template>
```
**Client/Server only**:
```
components/
├── Comments.client.vue → Only rendered on client
└── ServerData.server.vue → Only rendered on server
```
### `app/composables/`
Vue composables auto-imported (top-level files only):
```
composables/
├── useAuth.ts → useAuth()
├── useFoo.ts → useFoo()
└── nested/
└── utils.ts → NOT auto-imported
```
Re-export nested composables:
```ts
// 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 → /pricing
```
**Pages 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
```
```vue
<!-- layouts/default.vue -->
<template>
<div>
<TheHeader />
<slot />
<TheFooter />
</div>
</template>
```
Use in pages:
```vue
<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 plugin
```
### `server/` Directory
Nitro server code (stays at the project root, not under `app/`):
```
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
├── types/
│ └── todo.ts → Auto-imported types (server context only)
└── utils/
└── db.ts → Auto-imported server utilities
```
**Nuxt 5 server imports:** Nitro/h3 helpers (`defineEventHandler`, `getQuery`, `createError`…) are **no longer auto-imported by default** — import them from `nuxt/server`. Your own `server/utils/` and `server/types/` exports are still auto-imported, but only files directly in `server/utils/` (nested ones warn `NUXT_B9006`; re-export them from a top-level file). Handlers must default-export `defineEventHandler` from `nuxt/server` and can return JSON values, a `Response`, or a `ReadableStream` (no `sendStream`). Server KV storage is `useKV()` with `nitro.kv` (was `useStorage()`/`nitro.storage`). File names in `server/` cannot contain unknown dotted suffixes (`hello.world.ts` fails; add to `nitro.scanSuffixes`). Inside server code, import shared Nuxt composables from `#imports/server` (not `#imports`). Re-enable Nitro auto-imports with `experimental.nitroAutoImports: true`.
### `app/spa-loading-template.html`
Plain HTML (no Vue, no auto-imports; inline its own `<style>`) inserted into the server response for `ssr: false` pages, shown until the first page is ready, in a `<div id="__nuxt-loader">` sibling of `#__nuxt` (customize with `app.spaLoaderTag`/`app.spaLoaderAttrs`, keep the `id`). Config: `spaLoadingTemplate: 'my-loader.html'` (path relative to `srcDir`) | `true` (built-in screen) | `false`. `experimental.spaLoadingTemplateLocation: 'within'` renders it inside `#__nuxt`, removed as soon as Vue mounts. For route transitions use `<NuxtLoadingIndicator>`.
### `shared/` Directory
Code usable in **both** the Vue app and the Nitro server (Nuxt 3.14+). Cannot import any Vue or Nitro code.
```
shared/
├── utils/
│ └── format.ts → Auto-imported in app AND server
└── types/
└── api.ts → Auto-imported types
```
Only top-level files in `shared/utils/` and `shared/types/` are auto-imported. Import anything else via the `#shared` alias:
```ts
import { capitalize } from '#shared/utils/format'
import lower from '#shared/formatters/lower'
```
### `public/` Directory
Static assets served at root URL:
```
public/
├── favicon.ico → /favicon.ico
├── robots.txt → /robots.txt
└── images/
└── logo.png → /images/logo.png
```
### `app/assets/` Directory
Build-processed assets (under `app/` in Nuxt 4):
```
app/assets/
├── css/
│ └── main.css
├── images/
│ └── hero.png
└── fonts/
└── custom.woff2
```
Reference in components:
```vue
<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
- https://nuxt.com/docs/directory-structure/shared
- https://nuxt.com/docs/directory-structure/app/spa-loading-template
- https://nuxt.com/docs/guide/going-further/server-imports
- https://nuxt.com/docs/api/nuxt-config#alias
-->
references/core-modules.md---
name: nuxt-modules
description: Creating and using Nuxt modules to extend framework functionality
---
# 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`:
```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
```ts
// 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
```ts
// 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
```ts
// 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
```ts
// 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:
```ts
// modules/analytics/runtime/plugin.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('page:finish', () => {
console.log('Page loaded')
})
})
```
### Adding Server Routes
```ts
// 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
```ts
// 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
```ts
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:
```ts
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:
```ts
// 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.ts
```
Auto-registered or manually added:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'~/modules/my-module', // Explicit
],
})
```
## Module Dependencies
```ts
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/guide/modules/module-dependencies
- https://nuxt.com/docs/guide/modules/server-compatibility
- https://nuxt.com/docs/api/kit
-->
references/core-routing.md---
name: routing
description: File-based routing, dynamic routes, navigation, and middleware in Nuxt
---
# Routing
Nuxt uses file-system routing based on vue-router. Files in `app/pages/` automatically create routes.
## Nuxt 5 Routing Defaults
- **Case-sensitive matching** (`router.options.sensitive: true`) — `/About` no longer matches `pages/about.vue`. Match link casing to file names, or set `router: { options: { sensitive: false } }`. `sensitive` can only be set in `nuxt.config` (the build fails if `app/router.options.ts` sets it). With `sensitive: false`, route rules match lower-cased paths, so regex constraints in rule patterns must be written for lower-case (`[a-z]`, not `[A-Z]`).
- **Typed pages on by default** (`experimental.typedPages`) — `useRoute`, `navigateTo`, `<NuxtLink :to>`, and `router.push` are type-checked against your real routes. Referencing a route that doesn't exist is a type error. Opt out with `experimental.typedPages: false`.
- **Normalized page component names** — a page is named after its route, not its filename (`pages/foo/index.vue` → `foo`, not `index`). Update any `<KeepAlive :include>` filters to route names, or set `experimental.normalizePageNames: false`.
- **Serializable page meta extracted at build time** — every JSON-serializable `definePageMeta()` property is written into the generated route record; only non-serializable values (functions, refs, spreads) resolve at runtime.
- **Route rule patterns use URLPattern** - `'/blog/*'` now matches nested paths (`/blog/2026/hello`); use `'/blog/:slug'` for one segment. Only one `*`/`**` per key (use `/:post/comments/**`). Regex constraints cannot match `%` or percent-encoded chars, or use `\b`. Rules also support `proxy: 'https://api.example.com/**'`.
- **Unmatched client navigations load as a full document** (`experimental.serverPathFallback`) so `<NuxtLink to="/file.pdf">` reaches `public/` files or server routes.
- **Build-time link check** - `experimental.templateDiagnostics` warns (`NUXT_B4025`) when a static `<NuxtLink to>` matches no page route.
## Basic Routing
```
pages/
├── index.vue → /
├── about.vue → /about
└── posts/
├── index.vue → /posts
└── [id].vue → /posts/:id
```
## Dynamic 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:
```vue
<script setup lang="ts">
const route = useRoute()
// /posts/123 → route.params.id = '123'
console.log(route.params.id)
</script>
```
## Navigation
### NuxtLink Component
```vue
<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
```vue
<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
```ts
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const isAuthenticated = false // Your auth logic
if (!isAuthenticated) {
return navigateTo('/login')
}
})
```
Apply to pages:
```vue
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
// or multiple: middleware: ['auth', 'admin']
})
</script>
```
### Global Middleware
Name files with `.global` suffix:
```ts
// middleware/logging.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
console.log('Navigating to:', to.path)
})
```
### Inline Middleware
```vue
<script setup lang="ts">
definePageMeta({
middleware: [
function (to, from) {
// Inline middleware logic
},
],
})
</script>
```
## Page Meta
Configure page-level options:
```vue
<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
```vue
<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/`:
```vue
<!-- layouts/default.vue -->
<template>
<div>
<header>Header</header>
<slot />
<footer>Footer</footer>
</div>
</template>
```
```vue
<!-- layouts/admin.vue -->
<template>
<div class="admin">
<AdminSidebar />
<main>
<slot />
</main>
</div>
</template>
```
Use in pages:
```vue
<script setup lang="ts">
definePageMeta({
layout: 'admin',
})
</script>
```
Dynamic layout:
```vue
<script setup lang="ts">
const layout = ref('default')
function enableAdmin() {
setPageLayout('admin')
}
</script>
```
### Passing Props to Layouts (Nuxt 4.4+)
Use the object syntax for `layout` to pass typed props to the layout component:
```vue
<!-- app/pages/dashboard.vue -->
<script setup lang="ts">
definePageMeta({
layout: { name: 'panel', props: { sidebar: true, title: 'Dashboard' } },
})
</script>
```
```vue
<!-- app/layouts/panel.vue -->
<script setup lang="ts">
const props = defineProps<{ sidebar?: boolean, title?: string }>()
</script>
```
Props are fully typed from the layout's `defineProps`. Also works dynamically: `setPageLayout('panel', { sidebar: true })`.
## Named Views
A route can render into multiple `<NuxtPage>` outlets. Add named view files with the `name@view.vue` convention next to the default route file:
```
pages/
├── parent.vue
└── parent/
├── child.vue → default <NuxtPage />
└── child@sidebar.vue → <NuxtPage name="sidebar" />
```
```vue
<!-- pages/parent.vue -->
<template>
<NuxtPage />
<aside><NuxtPage name="sidebar" /></aside>
</template>
```
`definePageMeta` is read from the default route file only.
## Navigation Hooks
```vue
<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
- https://nuxt.com/docs/api/utils/define-page-meta
- https://nuxt.com/docs/getting-started/upgrade
- https://nuxt.com/docs/guide/recipes/custom-routing
- https://nuxt.com/docs/guide/concepts/rendering
-->
references/features-components-autoimport.md---
name: components-auto-imports
description: Auto-imported components, lazy loading, and hydration strategies
---
# 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 />
```
```vue
<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
```ts
// nuxt.config.ts
export default defineNuxtConfig({
components: [
{
path: '~/components',
pathPrefix: false, // Use filename only
},
],
})
```
With `pathPrefix: false`:
```
components/base/Button.vue → <Button />
```
### Grouping Directories
Wrap a directory name in parentheses to group files without affecting the component name:
```
components/base/(foo)/Button.vue → <BaseButton />
```
### `pattern` / `ignore` Globs
Each `components` dir entry accepts `pattern` and `ignore` globs to scan non-standard layouts (e.g. domain-driven structure). When `pattern` is set, the `extensions` option is ignored:
```ts
export default defineNuxtConfig({
components: [
// ~/domains/blog/components/PostCard.vue => <PostCard />
{ path: '~/domains', pattern: '*/components/**', pathPrefix: false },
],
})
```
## Lazy Loading
Prefix with `Lazy` for dynamic imports:
```vue
<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:
```vue
<template>
<LazyComments hydrate-on-visible />
</template>
```
### `hydrate-on-idle`
Hydrate when browser is idle:
```vue
<template>
<LazyAnalytics hydrate-on-idle />
</template>
```
### `hydrate-on-interaction`
Hydrate on user interaction:
```vue
<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:
```vue
<template>
<LazyMobileMenu hydrate-on-media-query="(max-width: 768px)" />
</template>
```
### `hydrate-after`
Hydrate after delay (milliseconds):
```vue
<template>
<LazyAds :hydrate-after="3000" />
</template>
```
### `hydrate-when`
Hydrate on condition:
```vue
<script setup lang="ts">
const isReady = ref(false)
</script>
<template>
<LazyEditor :hydrate-when="isReady" />
</template>
```
### `hydrate-never`
Never hydrate (static only):
```vue
<template>
<LazyStaticFooter hydrate-never />
</template>
```
### Hydration Event
```vue
<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
```
```vue
<template>
<!-- Only rendered in browser -->
<BrowserChart />
</template>
```
### Server-only (`.server.vue`)
```
components/
└── ServerMarkdown.server.vue
```
```vue
<template>
<!-- Rendered on server, not hydrated -->
<ServerMarkdown :content="markdown" />
</template>
```
Requires experimental flag:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
experimental: {
componentIslands: true,
},
})
```
### Paired Components
```
components/
├── Comments.client.vue # Browser version
└── Comments.server.vue # SSR version
```
Server version renders during SSR, client version takes over after hydration.
## Dynamic Components
```vue
<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:
```vue
<script setup lang="ts">
import { LazyMountainsList, NuxtLink } from '#components'
</script>
```
## Custom Directories
```ts
// 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):
```ts
// nuxt.config.ts
export default defineNuxtConfig({
components: {
global: true,
dirs: ['~/components'],
},
})
```
Or use `.global.vue` suffix:
```
components/
└── Icon.global.vue → Available globally
```
## Disabling Component Auto-imports
```ts
// nuxt.config.ts
export default defineNuxtConfig({
components: {
dirs: [], // Disable auto-imports
},
})
```
## Library Authors
Register components from npm package:
```ts
// 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
-->
references/features-components.md---
name: built-in-components
description: NuxtLink, NuxtPage, NuxtLayout, and other built-in Nuxt components
---
# Built-in Components
Nuxt provides several built-in components for common functionality.
## NuxtLink
Optimized link component with prefetching:
```vue
<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):
```vue
<!-- app/app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
```
With page transitions:
```vue
<template>
<NuxtPage :transition="{ name: 'fade', mode: 'out-in' }" />
</template>
```
Pass props to page:
```vue
<template>
<NuxtPage :page-key="route.fullPath" :foobar="123" />
</template>
```
## NuxtLayout
Controls layout rendering:
```vue
<!-- app/app.vue -->
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
```
Dynamic layout:
```vue
<template>
<NuxtLayout :name="layout">
<NuxtPage />
</NuxtLayout>
</template>
<script setup>
const layout = computed(() => isAdmin ? 'admin' : 'default')
</script>
```
Layout with transitions:
```vue
<template>
<NuxtLayout :transition="{ name: 'slide', mode: 'out-in' }">
<NuxtPage />
</NuxtLayout>
</template>
```
## NuxtLoadingIndicator
Progress bar for page navigation:
```vue
<!-- app/app.vue -->
<template>
<NuxtLoadingIndicator
color="repeating-linear-gradient(to right, #00dc82 0%, #34cdfe 50%, #0047e1 100%)"
finish-color="#00dc82"
:height="3"
:duration="2000"
:throttle="200"
/>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
```
## NuxtErrorBoundary
Catch and handle errors in child components:
```vue
<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:
```vue
<template>
<ClientOnly>
<!-- Browser-only component -->
<BrowserOnlyChart :data="chartData" />
<template #fallback>
<p>Loading chart...</p>
</template>
</ClientOnly>
</template>
```
## DevOnly
Render content only in development:
```vue
<template>
<DevOnly>
<DebugPanel />
</DevOnly>
</template>
```
## NuxtIsland
Server components (experimental):
```vue
<template>
<NuxtIsland name="HeavyComponent" :props="{ data: complexData }" />
</template>
```
Islands render in their own isolated Vue app keyed on props (for cacheability). `useRoute()` and other vue-router composables inside an island reflect the island's own request, **not** the current page — pass route info in via props or the `context` prop. Props/context are sent as GET query params (may appear in logs/CDN caches). `provide`/`inject` do not cross the island boundary (pass props or use a plugin). Treat props as untrusted: clamp numeric/array props that drive `v-for` (Nuxt caps numeric `v-for` at 10,000 in islands), use `defineOptions({ inheritAttrs: false })` on islands with polymorphic roots, and never spread uncontrolled objects like `route.query` onto islands (fall-through `onclick`-style attributes run as JS).
## NuxtImg and NuxtPicture
Optimized images (requires `@nuxt/image` module):
```vue
<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:
```vue
<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:
```vue
<template>
<ClientOnly>
<Teleport to="#teleports">
<Modal />
</Teleport>
</ClientOnly>
</template>
```
## NuxtRouteAnnouncer
Accessibility: automatically announces page changes (from the page `<title>`) to screen readers:
```vue
<!-- app/app.vue -->
<template>
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
```
## NuxtAnnouncer
Accessibility: a hidden live region for **manual** announcements of dynamic content (form validation, toasts, loading). Add it once, then drive it with the `useAnnouncer` composable (Nuxt 4.4.2+):
```vue
<!-- app/app.vue -->
<template>
<NuxtAnnouncer />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
```
```vue
<script setup lang="ts">
const { polite, assertive } = useAnnouncer()
assertive('Error: form is invalid') // interrupts immediately
</script>
```
Props: `politeness` (`polite` | `assertive` | `off`, default `polite`), `atomic` (default `true`). Use `<NuxtRouteAnnouncer>` for route changes, `<NuxtAnnouncer>` for everything else.
<!--
Source references:
- https://nuxt.com/docs/4.x/api/components/nuxt-link
- https://nuxt.com/docs/4.x/api/components/nuxt-page
- https://nuxt.com/docs/4.x/api/components/nuxt-layout
- https://nuxt.com/docs/4.x/api/components/client-only
- https://nuxt.com/docs/4.x/api/components/nuxt-announcer
- https://nuxt.com/docs/4.x/api/components/nuxt-island
-->
references/features-composables.md---
name: composables-auto-imports
description: Auto-imported Vue APIs, Nuxt composables, and custom utilities
---
# Composables Auto-imports
Nuxt automatically imports Vue APIs, Nuxt composables, and your custom composables/utilities.
## Built-in Auto-imports
### Vue APIs
```vue
<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
```vue
<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' })
// Client-only, runs when app is initialized and browser idle (use in .client plugins)
onNuxtReady(() => { /* heavy init */ }, { timeout: 2000 }) // timeout caps idle wait
// Accessibility — announce content to screen readers
const { polite, assertive } = useAnnouncer() // needs <NuxtAnnouncer> (Nuxt 4.4+)
const { set } = useRouteAnnouncer() // automatic on navigation
// Request helpers (SSR)
const headers = useRequestHeaders()
const event = useRequestEvent()
const url = useRequestURL()
</script>
```
> **Nuxt 4 / Nitro:** in server routes, `useRuntimeConfig()` is called without the `event` argument.
### useAnnouncer (Accessibility)
Manually announce dynamic content (form validation, toasts, loading) to screen readers. Requires `<NuxtAnnouncer>` in your app. Available in Nuxt 4.4.2+.
```vue
<script setup lang="ts">
const { polite, assertive } = useAnnouncer()
async function submit() {
try {
await $fetch('/api/contact', { method: 'POST', body: form })
polite('Message sent successfully') // waits for screen reader silence
} catch {
assertive('Error: failed to send') // interrupts immediately
}
}
</script>
```
Use `useRouteAnnouncer` (with `<NuxtRouteAnnouncer>`) instead for automatic route-change announcements based on the page `<title>`.
## Custom Composables (`app/composables/`)
### Creating Composables
```ts
// composables/useCounter.ts
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
```
```ts
// 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
```vue
<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:
```ts
// composables/index.ts
export { useHelper } from './nested/helper'
```
Or configure scanning:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
imports: {
dirs: [
'composables',
'composables/**', // Scan all nested
],
},
})
```
## Utilities (`app/utils/`)
```ts
// 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)
}
```
```vue
<script setup lang="ts">
// Auto-imported
const date = formatDate(new Date())
const price = formatCurrency(99.99)
</script>
```
## Server Utils (`server/utils/`)
```ts
// 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)
}
```
```ts
// server/api/users.ts
export default defineEventHandler(() => {
const db = useDb() // Auto-imported
return db.query('SELECT * FROM users')
})
```
Only files directly in `server/utils/` are scanned; re-export subdirectory helpers from a top-level file.
## Shared Utilities (`shared/`)
Code auto-imported in **both** the Vue app and the Nitro server (Nuxt 3.14+). Only `shared/utils/` and `shared/types/` top-level files are auto-imported. Must not import Vue or Nitro code.
```ts
// shared/utils/capitalize.ts
export function capitalize(input: string) {
return input ? input[0].toUpperCase() + input.slice(1) : ''
}
```
```ts
// Auto-imported in app components AND server/api/*
const title = capitalize('hello')
// Non-scanned files: import via the #shared alias
import lower from '#shared/formatters/lower'
```
## Third-party Package Imports
```ts
// 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:
```vue
<script setup lang="ts">
import { ref, computed, useFetch } from '#imports'
</script>
```
## Composable Context Rules
Nuxt composables must be called in valid context:
```ts
// ❌ Wrong - module level
const config = useRuntimeConfig()
export function useMyComposable() {}
```
```ts
// ✅ Correct - inside function
export function useMyComposable() {
const config = useRuntimeConfig()
return { apiBase: config.public.apiBase }
}
```
**Valid contexts:**
- `<script setup>` block
- `setup()` function
- `defineNuxtPlugin()` callback
- `defineNuxtRouteMiddleware()` callback
## Disabling Auto-imports
```ts
// 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/4.x/guide/concepts/auto-imports
- https://nuxt.com/docs/4.x/directory-structure/app/composables
- https://nuxt.com/docs/4.x/directory-structure/app/utils
- https://nuxt.com/docs/4.x/directory-structure/shared
- https://nuxt.com/docs/4.x/api/composables/use-announcer
-->
references/features-server.md---
name: server-routes
description: API routes, server middleware, and Nitro server engine in Nuxt
---
# Server Routes
Nuxt includes the Nitro server engine for building full-stack applications with API routes and server middleware. Nuxt 5 runs on **Nitro v3** (built on h3 v2 and srvx, using Web-standard `Request`/`Response`).
## Nuxt 5: `nuxt/server` Imports (auto-imports off by default)
In Nuxt 5, Nitro's server helpers (`defineEventHandler`, `getQuery`, `readBody`, cookie/header helpers, `createError`, `sendRedirect`, `getRouteRules`, `useRuntimeConfig`, sessions…) are **no longer auto-imported by default**. Import them explicitly from **`nuxt/server`** — the runtime-agnostic surface that resolves to whatever server builder is configured and survives h3/Nitro majors:
```ts
// server/api/hello.ts
import { defineEventHandler, getQuery } from 'nuxt/server'
export default defineEventHandler((event) => {
const { name } = getQuery<{ name?: string }>(event)
return { message: `Hello, ${name ?? 'world'}!` }
})
```
- Your own exports from `server/utils/` and `shared/utils/` are still auto-imported.
- Server code should import shared Nuxt composables from `#imports/server` (not `#imports`).
- Re-enable Nitro auto-imports while migrating: `experimental.nitroAutoImports: true`.
- `defineEventHandler` preserves your handler's return type — this is what types `$fetch`/`useFetch` calls to the route. Annotate the returned value, not the handler.
- Helpers not in the `nuxt/server` surface (e.g. `readMultipartFormData`, `lazyEventHandler`, `useKV`, `defineCachedHandler`, `defineTask`) still come from `nitro/h3`, `nitro/kv`, `nitro/cache`, `nitro/task`, etc.
- Also in `nuxt/server`: `getRequestHost`/`getRequestProtocol` (pass `{ xForwardedHost: true, xForwardedProto: true }` only behind a trusted proxy), `parseCookies`, `matchRouteRules` (rules for another path), `serverFetch(event, path)` (in-process fetch of an app route; forwards `cookie`/`authorization`), and `useServerHooks()` (renderer hooks like `render:html`). `toNuxtRequestEvent` was removed.
- A handler file must default-export `defineEventHandler` from `nuxt/server` (not h3's). It may return JSON-serializable data, a `Response`, or a `ReadableStream`. For a handler needing the runtime's own event (`proxyRequest`, `event.waitUntil()`, ...), import `defineEventHandler` from `nitro/h3` instead (Nitro v3 only, register as `nitro3`).
- Only files directly in `server/utils/` are auto-imported; re-export from a top-level file for subdirectory helpers.
- Don't import `nuxt/server` in Nuxt-process code (`nuxt.config.ts`, module `setup`, dev server handlers): runtime/app config come back empty. Read `nuxt.options` there.
## API Routes
Create files in `server/api/` directory:
```ts
// server/api/hello.ts
import { defineEventHandler } from 'nuxt/server'
export default defineEventHandler((event) => {
return { message: 'Hello World' }
})
```
Access at `/api/hello`. Return typing flows automatically to `$fetch('/api/hello')`.
> The examples below omit the `import` lines for brevity; add them from `nuxt/server` (or enable `nitroAutoImports`).
### HTTP Methods
```ts
// 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
```ts
// 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
```ts
// 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
```ts
// 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
```ts
// server/api/auth.ts
import { defineEventHandler, getCookie, getRequestHeader, setCookie } from 'nuxt/server'
export default defineEventHandler((event) => {
// Read a request header
const auth = getRequestHeader(event, 'authorization')
// Read a cookie
const token = getCookie(event, 'token')
// Set a response header (Web Headers API)
event.res.headers.set('X-Custom-Header', 'value')
// Set a cookie
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:
```ts
// 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.req.method} ${event.url.pathname}`)
})
```
Access context in routes:
```ts
// server/api/profile.ts
export default defineEventHandler((event) => {
const user = event.context.user
if (!user) {
throw createError({ status: 401, message: 'Unauthorized' })
}
return user
})
```
## Error Handling
Nitro v3 / h3 v2 rename the error fields to Web-standard names: `status`/`statusText` (was `statusCode`/`statusMessage`):
```ts
// server/api/users/[id].ts
import { createError, defineEventHandler, getRouterParam } from 'nuxt/server'
export default defineEventHandler((event) => {
const id = getRouterParam(event, 'id')
const user = findUser(id)
if (!user) {
throw createError({
status: 404,
statusText: 'User not found',
})
}
return user
})
```
Notes for Nitro v3:
- The server error class is now `HTTPError` (from `nitro/h3`); `createError` from `nuxt/server` builds a `NuxtError`. `NuxtError` is no longer a subclass of `HTTPError`, so narrow with `HTTPError.isError(error)` or `isNuxtError(error)` rather than `instanceof HTTPError`.
- `useRuntimeConfig()` no longer accepts (or needs) the `event` argument in server routes.
- The `H3Event` uses Web-standard APIs: `event.url.pathname` (not `event.path`), `event.req.method`/`event.req.headers` (Web `Headers`), `event.res.status`/`event.res.headers.set(...)` (not `event.node.res`).
## Request Validation
`readValidatedBody` / `getValidatedQuery` accept any [Standard Schema](https://standardschema.dev) (Zod, Valibot, ArkType). Validated shapes also type the matching `$fetch`/`useFetch` request:
```ts
// server/api/users.post.ts
import { defineEventHandler, readValidatedBody } from 'nuxt/server'
import { z } from 'zod'
export default defineEventHandler(async (event) => {
const user = await readValidatedBody(event, z.object({ name: z.string() }))
return { created: user.name }
})
```
Invalid input is rejected with a `400` whose `data.issues` lists the failures.
## Sessions
`useSession` (and `getSession`/`updateSession`/`clearSession`) come from `nuxt/server` and seal a sealed cookie session with `iron-webcrypto`, so they run under any server builder. No `password` is required — without one the session is sealed with a secret derived from `appSecret` (`NUXT_APP_SECRET`); pass a `password` of ≥32 chars to override. Default cookie name is `nuxt-session`.
```ts
// server/api/visits.ts
import { defineEventHandler, useSession } from 'nuxt/server'
export default defineEventHandler(async (event) => {
const session = await useSession<{ visits: number }>(event)
await session.update(data => ({ visits: (data.visits ?? 0) + 1 }))
return { visits: session.data.visits }
})
```
> h3 ships its own same-named session helpers (`import { useSession } from 'nitro/h3'`); they are a separate, non-interchangeable implementation (cookie name `h3`).
## Server Utils
Auto-imported in `server/utils/`:
```ts
// server/utils/db.ts
export function useDb() {
return createDbConnection()
}
```
```ts
// 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. In Nitro v3 the factory is `definePlugin` from `nitro`:
```ts
// server/plugins/db.ts
import { definePlugin } from 'nitro'
export default definePlugin((nitroApp) => {
// Initialize database connection
const db = createDbConnection()
// Add to context
nitroApp.hooks.hook('request', (event) => {
event.context.db = db
})
})
```
> The `beforeResponse`/`afterResponse` hooks are replaced by a single `response` hook in Nitro v3.
## Streaming Responses
```ts
// server/api/stream.ts
export default defineEventHandler((event) => {
event.res.headers.set('Content-Type', 'text/event-stream')
event.res.headers.set('Cache-Control', 'no-cache')
event.res.headers.set('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
})
```
## Nitro Builder
Nitro bundles the server with Rolldown by default. With the webpack/Rspack app builder, pick another via `nitro.builder` (or `NITRO_BUILDER`), e.g. `builder: 'rspack', nitro: { builder: 'rspack' }`. The Nitro `rspack` and `bun` builders are experimental; with `experimental.nitroViteEnvironment`, Vite builds the server and `nitro.builder` is ignored.
## Server Storage
Key-value storage with multiple drivers. In Nitro v3 it is the KV layer: `useKV` comes from `nitro/kv` (was `useStorage`) and mounts are configured with `nitro.kv` (was `nitro.storage`):
```ts
// server/api/cache.ts
import { defineEventHandler } from 'nuxt/server'
import { useKV } from 'nitro/kv'
export default defineEventHandler(async (event) => {
const storage = useKV()
// Set value
await storage.setItem('key', { data: 'value' })
// Get value
const data = await storage.getItem('key')
return data
})
```
Configure KV drivers in `nuxt.config.ts`:
```ts
export default defineNuxtConfig({
nitro: {
kv: {
redis: {
driver: 'redis',
url: 'redis://localhost:6379',
},
},
},
})
```
<!--
Source references:
- https://nuxt.com/docs/getting-started/server
- https://nuxt.com/docs/directory-structure/server
- https://nuxt.com/docs/guide/going-further/server-imports
- https://nuxt.com/docs/getting-started/upgrade#migration-to-nitro-v3
- https://nuxt.com/docs/guide/modules/server-compatibility
- https://nitro.build/blog/v3-beta
-->
references/features-state.md---
name: state-management
description: useState composable and SSR-friendly state management in Nuxt
---
# 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:
```vue
<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:
```ts
// composables/useUser.ts
export function useUser() {
return useState<User | null>('user', () => null)
}
export function useLocale() {
return useState('locale', () => 'en')
}
```
```vue
<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:
```vue
<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
```ts
// ❌ Wrong - causes memory leaks and shared state across requests
export const globalState = ref({ user: null })
```
### ✅ Use useState Instead
```ts
// ✅ Correct - SSR-safe
export const useGlobalState = () => useState('global', () => ({ user: null }))
```
## Clearing State
```ts
// Clear specific state
clearNuxtState('counter')
// Clear multiple states
clearNuxtState(['counter', 'user'])
// Clear all state (use with caution)
clearNuxtState()
```
> **Nuxt 5:** `clearNuxtState` now **resets state to its `useState` init value** (via `experimental.defaults.useState.resetOnClear`, on by default) instead of setting it to `undefined`. Check for the initial value after clearing, or set `resetOnClear: false` to restore the old behavior.
## With Pinia
For complex state management, use Pinia:
```bash
npx nuxi module add pinia
```
```ts
// stores/counter.ts
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
},
})
```
```ts
// 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 }
})
```
```vue
<script setup lang="ts">
const counterStore = useCounterStore()
const userStore = useUserStore()
// Initialize store data once
await callOnce(async () => {
await userStore.fetchUser()
})
</script>
```
## Advanced: Locale Example
```ts
// 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
```ts
// ❌ 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
-->
references/rendering-modes.md---
name: rendering-modes
description: Universal rendering, client-side rendering, and hybrid rendering in Nuxt
---
# 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:
```ts
// 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:
```ts
// 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:
```html
<!-- 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:
```ts
// 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` | Stale-while-revalidate caching |
| `isr: number \| true` | Incremental static regeneration |
| `cache: { maxAge: number }` | Cache with TTL |
| `redirect: string` | Redirect to another path |
| `cors: true` | Add CORS headers |
| `headers: object` | Custom response headers |
> **Nitro v3 changes:**
> - Redirect rules use `status` (not `statusCode`): `redirect: { to: '/new', status: 302 }`.
> - Cached routes (`cache`/`swr`/`isr`) now key on **path only** — the query string is dropped. Keep it with `allowQuery: true` or a list of param names: `cache: { swr: true, maxAge: 60, allowQuery: ['page'] }`.
### Inline Route Rules
Define per-page:
```vue
<script setup lang="ts">
defineRouteRules({
prerender: true,
})
</script>
```
## Prerendering
Generate static HTML at build time:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
// Prerender specific routes
routeRules: {
'/': { prerender: true },
'/about': { prerender: true },
'/posts/*': { prerender: true },
},
})
```
Or use `nuxt generate`:
```bash
nuxt generate
```
### Nuxt 5: Payload Extraction & Error Pages
- **Payload extraction defaults to `'client'`** (`experimental.payloadExtraction`): the payload of a prerendered/cached page is **inlined in the HTML** for the initial render, and `_payload.json` is only fetched during client-side navigation. Forced to `false` when `ssr: false`.
- `nuxt generate` / `nuxt build --prerender` emit SPA fallbacks into `.output/public/`: **`200.html`** (client router handles unmatched paths) and **`404.html`** (keep 404 status + load app). A plain `nuxt build` does not.
- **Prerender the error page** with `experimental.prerenderErrorPages: true` — Nuxt renders `error.vue` at build time into `404.html`. Because one file serves every missing path, guard request-specific data with `import.meta.prerender` and wrap request-specific markup in `<ClientOnly>`.
### Programmatic Prerendering
```ts
// 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:
```ts
// server/api/posts.ts or a plugin
prerenderRoutes(['/posts/1', '/posts/2', '/posts/3'])
```
## Edge-Side Rendering
Render at CDN edge servers:
```ts
// 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`:
```vue
<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:
```vue
<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
-->
SKILL.md--- name: nuxt description: Nuxt full-stack Vue framework with SSR, auto-imports, and file-based routing. Use when working with Nuxt apps, server routes, useFetch, middleware, or hybrid rendering. metadata: author: Anthony Fu version: "2026.11.5" source: Generated from https://github.com/nuxt/nuxt, scripts located at https://github.com/antfu/skills --- 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 5.x (`5.0.0-0`), generated at 2026-11-05. > **Directory layout:** the default `srcDir` is `app/` — Vue app code (`app.vue`, `components/`, `composables/`, `pages/`, etc.) lives under `app/`, while `server/`, `shared/`, `public/`, `modules/`, `layers/` and `nuxt.config.ts` stay at the project root. The `~`/`@` aliases point at `app/`; use `~~`/`@@` for the root. > **Nuxt 5 note:** Nuxt 5 makes many former experimental flags the default. The biggest shifts for agents: server code moves to **Nitro v3** (h3 v2, Web-standard `Request`/`Response`), server utilities import from **`nuxt/server`** and are no longer auto-imported by default, `$fetch`/`useFetch` are **typed from generated route types**, routing is **case-sensitive** and **typed** by default, and the bundler is **Vite 8 (Rolldown)**. Nuxt 5 requires Node `22.19`/`24.11`+. A Nitro v2 compatibility layer (`nitroLegacy`) keeps older server code building during migration. See individual references for details. ## Core | Topic | Description | Reference | |-------|-------------|-----------| | Directory Structure | Nuxt 4 `app/` srcDir, `shared/`, aliases, conventions | [core-directory-structure](references/core-directory-structure.md) | | Configuration | nuxt.config.ts, app.config.ts, aliases, Nuxt 5 defaults, jiti removal, experimental | [core-config](references/core-config.md) | | CLI Commands | Dev server, build, generate, preview, and utility commands | [core-cli](references/core-cli.md) | | Routing | File-based routing, case-sensitive + typed routes, named views, layout props, middleware | [core-routing](references/core-routing.md) | | Data Fetching | useFetch, useAsyncData, $fetch, typed routes, createUseFetch factories, caching | [core-data-fetching](references/core-data-fetching.md) | | Modules | Creating and using Nuxt modules, Nuxt Kit utilities | [core-modules](references/core-modules.md) | | Deployment | Platform-agnostic deployment with Nitro, Vercel, Netlify, Cloudflare | [core-deployment](references/core-deployment.md) | ## Features | Topic | Description | Reference | |-------|-------------|-----------| | Composables Auto-imports | Vue/Nuxt composables, custom composables, `shared/`, useAnnouncer | [features-composables](references/features-composables.md) | | Components Auto-imports | Component naming, lazy loading, hydration strategies | [features-components-autoimport](references/features-components-autoimport.md) | | Built-in Components | NuxtLink, NuxtPage, NuxtLayout, NuxtAnnouncer, ClientOnly, and more | [features-components](references/features-components.md) | | State Management | useState composable, SSR-friendly state, Pinia integration | [features-state](references/features-state.md) | | Server Routes | API routes, `nuxt/server` imports, Nitro v3 (h3 v2), sessions, middleware | [features-server](references/features-server.md) | ## Rendering | Topic | Description | Reference | |-------|-------------|-----------| | Rendering Modes | Universal (SSR), client-side (SPA), hybrid rendering, route rules | [rendering-modes](references/rendering-modes.md) | ## Best Practices | Topic | Description | Reference | |-------|-------------|-----------| | Data Fetching Patterns | Efficient fetching, caching, parallel requests, error handling | [best-practices-data-fetching](references/best-practices-data-fetching.md) | | SSR & Hydration | Avoiding context leaks, hydration mismatches, composable patterns | [best-practices-ssr](references/best-practices-ssr.md) | ## Advanced | Topic | Description | Reference | |-------|-------------|-----------| | Layers | Extending applications with reusable layers | [advanced-layers](references/advanced-layers.md) | | Lifecycle Hooks | Build-time, runtime, and server hooks | [advanced-hooks](references/advanced-hooks.md) | | Module Authoring | Publishable modules with Nuxt Kit, keyed composables, dependencies | [advanced-module-authoring](references/advanced-module-authoring.md) |