
Nuxt
- 7.3k installs
- 696 repo stars
- Updated July 27, 2026
- onmax/nuxt-skills
nuxt is an agent skill for Nuxt 4.3+ development with on-demand references for server routes routing middleware composables components and configuration.
About
nuxt is an agent skill for Nuxt 4.3+ projects providing progressive guidance on server routes file-based routing middleware plugins composables components and configuration. Reference files load on demand covering server.md for API routes validation with Zod WebSocket and SSE, routing.md for pages layouts and typed router, middleware-plugins.md for route guards and app lifecycle, nuxt-composables.md for useFetch and useRequestURL, nuxt-components.md for NuxtLink NuxtImg NuxtTime, and nuxt-config.md for modules auto-imports and layers. Quick start shows defineEventHandler with getValidatedQuery Zod parsing on server/api routes. Nuxt 4 differences include NuxtPage instead of Nuxt, getRouterParam instead of context.params, and useRequestURL origin instead of window.origin. The skill emphasizes token efficiency by loading only task-relevant reference files rather than all docs at once and points to official Nuxt h3 and Nitro sources for advanced cases. Related skills include vue for composables nuxt-ui nuxthub nuxt-content and nuxt-modules for specialized work.
- Nuxt 4.3+ guidance with progressive reference file loading by task.
- server.md covers h3 v1 API routes Zod validation WebSocket and SSE.
- Nuxt 4 migrations: NuxtPage getRouterParam useRequestURL typed router.
- Quick start defineEventHandler with getValidatedQuery Zod defaults.
- Related skills: vue nuxt-ui nuxthub nuxt-content nuxt-modules.
Nuxt by the numbers
- 7,252 all-time installs (skills.sh)
- +66 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #73 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
nuxt capabilities & compatibility
- Capabilities
- server api routes with h3 defineeventhandler and · file based routing pages layouts and typed route · route middleware and plugin lifecycle patterns · nuxt composables usefetch userequesturl navigati · nuxtlink nuxtimg nuxttime component guidance · nuxt.config modules auto imports and layers conf
- Use cases
- frontend · api development · devops
What nuxt says it does
Use when working on Nuxt 4+ projects - provides server routes, file-based routing, middleware patterns, Nuxt-specific composables, and configuration with latest docs.
`<Nuxt />` | `<NuxtPage />`
npx skills add https://github.com/onmax/nuxt-skills --skill nuxtAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7.3k |
|---|---|
| repo stars | ★ 696 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | onmax/nuxt-skills ↗ |
How do I implement Nuxt 4 server APIs file routing middleware and composables using current patterns instead of Nuxt 2 or 3 idioms?
Build Nuxt 4.3+ apps with server routes, file-based routing, middleware, composables, and progressive reference loading.
Who is it for?
Nuxt 4.3+ projects needing server routes typed routing middleware plugins or composable guidance without loading all docs.
Skip if: Nuxt 2 legacy patterns, non-Nuxt Vue SPA-only apps, or database setup without the nuxthub skill.
When should I use this skill?
User works on Nuxt 4 server routes pages middleware plugins composables or nuxt.config.ts modules.
What you get
Task-appropriate Nuxt 4 code and patterns loaded from the relevant reference files with correct h3 and Nitro conventions.
- Nuxt pages and layouts
- Server route handlers
- Middleware and plugin configs
By the numbers
- Updated for Nuxt 4.3+
- Covers h3 v1 and Nitropack v2 patterns
Files
Nuxt 4+ Development
Progressive guidance for Nuxt 4+ projects (v4.3+) with latest patterns and conventions.
When to Use
Working with:
- Server routes (API endpoints, server middleware, server utils)
- File-based routing (pages, layouts, route groups)
- Nuxt middleware (route guards, navigation)
- Nuxt plugins (app extensions)
- Nuxt-specific features (auto-imports, layers, modules)
Available Guidance
Read specific files based on current work:
- [references/server.md](references/server.md) - API routes, server middleware, validation (Zod), WebSocket, SSE
- [references/routing.md](references/routing.md) - File-based routing, route groups, typed router, definePage
- [references/middleware-plugins.md](references/middleware-plugins.md) - Route middleware, plugins, app lifecycle
- [references/nuxt-composables.md](references/nuxt-composables.md) - Nuxt composables (useRequestURL, useFetch, navigation)
- [references/nuxt-components.md](references/nuxt-components.md) - NuxtLink, NuxtImg, NuxtTime (prefer over HTML elements)
- [references/nuxt-config.md](references/nuxt-config.md) - Configuration, modules, auto-imports, layers
For Vue composables: See vue skill composables.md (VueUse, Composition API patterns) For UI components: use nuxt-ui skill For database/storage: use nuxthub skill For content-driven sites: use nuxt-content skill For creating modules: use nuxt-modules skill For project scaffolding/CI: use ts-library skill
Loading Files
Consider loading these reference files based on your task:
- [ ] references/server.md - if creating API endpoints or server middleware
- [ ] references/routing.md - if setting up pages, layouts, or route groups
- [ ] references/nuxt-composables.md - if using Nuxt composables (useFetch, useRequestURL, etc.)
- [ ] references/middleware-plugins.md - if working with middleware or plugins
- [ ] references/nuxt-components.md - if using Nuxt components (NuxtLink, NuxtImg, etc.)
- [ ] references/nuxt-config.md - if editing nuxt.config.ts
- [ ] references/project-setup.md - if setting up CI/ESLint/build tools
DO NOT load all files at once. Load only what's relevant to your current task.
Quick Start
// server/api/hello.get.ts
import { z } from 'zod'
export default defineEventHandler(async (event) => {
const { name } = await getValidatedQuery(event, z.object({
name: z.string().default('world'),
}).parse)
return { message: `Hello ${name}` }
})Nuxt 4 vs Older Versions
You are working with Nuxt 4+. Key differences:
| Old (Nuxt 2/3) | New (Nuxt 4) |
|---|---|
<Nuxt /> | <NuxtPage /> |
context.params | getRouterParam(event, 'name') |
window.origin | useRequestURL().origin |
| String routes | Typed router with route names |
| Separate layouts/ | Parent routes with <slot> |
If you're unsure about Nuxt 4 patterns, read the relevant guidance file first.
Latest Documentation
When to fetch latest docs:
- New Nuxt 4 features not covered here
- Module-specific configuration
- Breaking changes or deprecations
- Advanced use cases
Official sources:
- Nuxt: https://nuxt.com/docs
- h3 (server engine): https://v1.h3.dev/
- Nitro: https://nitro.build/
Token Efficiency
Main skill: ~300 tokens. Each sub-file: ~800-1500 tokens. Only load files relevant to current task.
Nuxt Middleware & Plugins
When to Use
Working with middleware/ or plugins/ directories, route guards, app extensions.
Route Middleware
Route middleware runs before navigation. Used for auth checks, redirects, logging.
Global Middleware
Runs on every route change. REQUIRED: Use `.global.ts` suffix:
// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return navigateTo('/login')
}
})Without `.global.ts` suffix, middleware is named (not global).
Red Flags - Stop and Check Skill
If you're thinking any of these, STOP and re-read this skill:
- "Suffix doesn't matter, it's about where I put it"
- "I'll redirect() instead of return navigateTo()"
- "I remember Nuxt 3 middleware patterns"
- "Export default function is simpler"
All of these mean: You're using outdated patterns. Use Nuxt 4 patterns instead.
Named Middleware
Runs only when explicitly applied. No .global suffix:
// middleware/admin.ts
export default defineNuxtRouteMiddleware((to, from) => {
const auth = useAuthStore()
if (!auth.isAdmin) {
return navigateTo('/')
}
})Apply in page:
<script setup lang="ts">
definePageMeta({
middleware: ['admin']
})
</script>Middleware Return Values
export default defineNuxtRouteMiddleware((to, from) => {
// Allow navigation
return
// Redirect
return navigateTo('/login')
// Abort navigation
return abortNavigation()
// Abort with error
return abortNavigation('Not authorized')
})Middleware Order
1. Global middleware (alphabetical by filename) 2. Layout middleware (if layout defines middleware) 3. Page middleware (defined in definePageMeta)
Plugins
Plugins extend Vue app with global functionality. Run during app initialization.
Basic Plugin
// plugins/my-plugin.ts
export default defineNuxtPlugin((nuxtApp) => {
return {
provide: {
hello: (name: string) => `Hello ${name}!`
}
}
})Use in components:
<script setup lang="ts">
const { $hello } = useNuxtApp()
console.log($hello('World')) // "Hello World!"
</script>Plugin with Vue Plugin
import type { PluginOptions } from 'vue-toastification'
// plugins/toast.client.ts
import Toast from 'vue-toastification'
import 'vue-toastification/dist/index.css'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(Toast, {
position: 'top-right',
timeout: 3000
} as PluginOptions)
})Plugin with Hooks
// plugins/init.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('app:created', () => {
console.log('App created')
})
nuxtApp.hook('page:finish', () => {
console.log('Page finished loading')
})
})Client-Only or Server-Only
Use file suffix:
.client.ts- runs only on client.server.ts- runs only on server
// plugins/analytics.client.ts
export default defineNuxtPlugin(() => {
// Only runs in browser
if (window.analytics) {
window.analytics.init()
}
})Plugin Order
Use numeric prefix for execution order:
plugins/
├── 01.first.ts
├── 02.second.ts
└── 03.third.tsAsync Plugins
// plugins/api.ts
export default defineNuxtPlugin(async (nuxtApp) => {
const config = await fetch('/api/config').then(r => r.json())
return {
provide: {
config
}
}
})Best Practices
Middleware:
- Return navigation or nothing - don't mutate state heavily
- Keep logic minimal - delegate to composables/stores
- Use for guards & redirects only
- Check meta properly -
to.meta.requiresAuth - Global = `.global.ts` suffix required
Plugins:
- Use for app-wide functionality only
- Provide via `provide` for type safety
- Consider client/server context - use
.client/.server - Minimize work in plugin initialization
- Use hooks for lifecycle events
Common Mistakes
| ❌ Wrong | ✅ Right |
|---|---|
export default function({ route }) | export default defineNuxtRouteMiddleware((to, from) => {}) |
| Mutate route object | Return navigateTo() or nothing |
middleware/auth.ts (not global) | middleware/auth.global.ts (global) |
redirect('/login') | return navigateTo('/login') |
| Plugin without defineNuxtPlugin | Wrap in defineNuxtPlugin() |
Middleware Example: Auth
// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
const auth = useAuthStore()
// Public routes
const publicRoutes = ['/', '/login', '/register']
if (publicRoutes.includes(to.path)) {
return
}
// Check auth
if (!auth.isAuthenticated) {
return navigateTo('/login')
}
// Check role
if (to.meta.requiresAdmin && !auth.isAdmin) {
return abortNavigation('Access denied')
}
})Plugin Example: API Client
// plugins/api.ts
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig()
const api = $fetch.create({
baseURL: config.public.apiBase,
onRequest({ request, options }) {
const auth = useAuthStore()
if (auth.token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${auth.token}`
}
}
},
onResponseError({ response }) {
if (response.status === 401) {
navigateTo('/login')
}
}
})
return {
provide: {
api
}
}
})Resources
- Nuxt middleware: https://nuxt.com/docs/guide/directory-structure/middleware
- Nuxt plugins: https://nuxt.com/docs/guide/directory-structure/plugins
- Route middleware: https://nuxt.com/docs/getting-started/routing#route-middleware
Nuxt Built-in Components
When to Use
Working with images, links, or time display in templates. Always prefer Nuxt components over HTML elements.
Component Preferences
| HTML Element | Nuxt Component | Why |
|---|---|---|
<a> | <NuxtLink> | Client-side navigation, prefetching |
<img> | <NuxtImg> | Optimization, lazy loading, responsive |
<time> | <NuxtTime> | SSR-safe formatting, localization |
NuxtLink
ALWAYS use `<NuxtLink>` instead of `<a>` for internal links:
<template>
<!-- Internal navigation -->
<NuxtLink to="/about">About</NuxtLink>
<NuxtLink :to="{ name: '/users/[userId]', params: { userId } }">Profile</NuxtLink>
<!-- External links (uses target="_blank" automatically with external) -->
<NuxtLink to="https://nuxt.com" external>Nuxt Docs</NuxtLink>
<!-- Prefetch control -->
<NuxtLink to="/dashboard" :prefetch="false">Dashboard</NuxtLink>
<!-- Active state styling -->
<NuxtLink to="/settings" active-class="text-primary" exact-active-class="font-bold">
Settings
</NuxtLink>
</template>Props:
to- Route path or route objectexternal- Force external link behaviortarget- Link target (_blank, etc.)prefetch- Enable/disable prefetching (default: true)noPrefetch- Disable prefetchingactiveClass- Class when route matchesexactActiveClass- Class when route exactly matches
Docs: https://nuxt.com/docs/api/components/nuxt-link
NuxtImg
ALWAYS use `<NuxtImg>` instead of `<img>` for images:
Requires @nuxt/image module (usually pre-installed).
<template>
<!-- Basic usage -->
<NuxtImg src="/images/hero.jpg" alt="Hero image" />
<!-- Responsive with sizes -->
<NuxtImg
src="/images/banner.jpg"
alt="Banner"
width="1200"
height="600"
sizes="100vw sm:50vw md:400px"
/>
<!-- Lazy loading (default) -->
<NuxtImg src="/images/photo.jpg" loading="lazy" alt="Photo" />
<!-- Eager loading for above-fold -->
<NuxtImg src="/images/logo.svg" loading="eager" alt="Logo" />
<!-- With placeholder blur -->
<NuxtImg src="/images/product.jpg" placeholder alt="Product" />
<!-- Provider-specific (Cloudinary, etc.) -->
<NuxtImg provider="cloudinary" src="/folder/image.jpg" width="500" />
<!-- Format conversion -->
<NuxtImg src="/images/photo.png" format="webp" alt="Photo" />
</template>Props:
src- Image source pathalt- Alt text (required for accessibility)width/height- Dimensionssizes- Responsive sizesloading-lazy(default) oreagerplaceholder- Show blur placeholder while loadingformat- Force output format (webp,avif, etc.)quality- Image quality (1-100)provider- Image provider (cloudinary, imgix, etc.)
For art direction, use `<NuxtPicture>` (different sources per breakpoint).
Docs: https://image.nuxt.com/usage/nuxt-img
NuxtTime
ALWAYS use `<NuxtTime>` instead of `<time>` or manual formatting:
<template>
<!-- Relative time -->
<NuxtTime :datetime="post.createdAt" relative />
<!-- Output: "2 hours ago" -->
<!-- Absolute with locale -->
<NuxtTime :datetime="event.date" locale="en-US" />
<!-- Custom format -->
<NuxtTime :datetime="date" year="numeric" month="long" day="numeric" />
<!-- Output: "December 6, 2025" -->
<!-- Short format -->
<NuxtTime :datetime="date" month="short" day="numeric" />
<!-- Output: "Dec 6" -->
<!-- With time -->
<NuxtTime :datetime="date" hour="numeric" minute="2-digit" />
</template>Props:
datetime- Date string, Date object, or timestamprelative- Show relative time ("2 hours ago")locale- Locale for formattingyear,month,day,hour,minute,second- Intl.DateTimeFormat options
Docs: https://nuxt.com/docs/api/components/nuxt-time
Common Mistakes
| ❌ Wrong | ✅ Right |
|---|---|
<a href="/about"> | <NuxtLink to="/about"> |
<img src="/photo.jpg"> | <NuxtImg src="/photo.jpg" alt="..."> |
<time>{{ formatDate(date) }}</time> | <NuxtTime :datetime="date" /> |
formatTimeAgo(date) in template | <NuxtTime :datetime="date" relative /> |
new Date().toLocaleDateString() | <NuxtTime :datetime="date" /> |
Best Practices
- NuxtLink for all internal routes - enables prefetching and client-side navigation
- NuxtImg for all images - automatic optimization, lazy loading, responsive
- NuxtTime for all dates - SSR-safe, automatic localization
- Always provide alt text for images
- Use `loading="eager"` for above-the-fold images
- Use sizes prop for responsive images
Resources
- NuxtLink: https://nuxt.com/docs/api/components/nuxt-link
- NuxtImg: https://image.nuxt.com/usage/nuxt-img
- NuxtPicture: https://image.nuxt.com/usage/nuxt-picture
- NuxtTime: https://nuxt.com/docs/api/components/nuxt-time
Nuxt Composables & Utilities
When to Use
Working with Nuxt-specific composables, URL handling, navigation, or data fetching.
URL & Request Handling
useRequestURL()
ALWAYS use `useRequestURL()` instead of `window.origin` or `window.location`:
// ✅ Correct - works SSR + client
const url = useRequestURL()
console.log(url.origin) // https://example.com
console.log(url.pathname) // /users/123
console.log(url.search) // ?tab=profile
// ❌ Wrong - breaks on SSR, not available server-side
const origin = window.origin
const path = window.location.pathnameWhy: window is undefined during SSR. useRequestURL() works everywhere.
useRequestURL() Patterns
// Get full URL
const url = useRequestURL()
const fullUrl = url.href // https://example.com/users/123?tab=profile
// Get origin (base URL)
const baseUrl = url.origin // https://example.com
// Get path
const path = url.pathname // /users/123
// Get query params (use useRoute() instead for better typing)
const params = url.searchParams
const tab = params.get('tab') // 'profile'
// Build absolute URL
const apiUrl = `${url.origin}/api/users`Navigation Composables
navigateTo()
// Navigate to route
await navigateTo('/about')
// Type-safe navigation
await navigateTo({ name: '/users/[userId]', params: { userId: '123' } })
// External URL
await navigateTo('https://nuxt.com', { external: true })
// Replace history
await navigateTo('/login', { replace: true })
// Open in new tab
await navigateTo('/docs', { open: { target: '_blank' } })
// Server-side redirect
return navigateTo('/login') // in middleware or server routeuseRouter()
const router = useRouter()
// Navigate
router.push({ name: '/users/[userId]', params: { userId: '123' } })
// Go back
router.back()
// Go forward
router.forward()
// Navigation guards
router.beforeEach((to, from) => {
// Guard logic
})useRoute()
// Generic route
const route = useRoute()
// Typed route (preferred)
const route = useRoute('/users/[userId]')
// Access params
const userId = route.params.userId
// Access query
const tab = route.query.tab
// Access meta
const requiresAuth = route.meta.requiresAuthData Fetching
useFetch()
// Basic fetch
const { data, error, pending, refresh } = await useFetch('/api/users')
// With params
const { data } = await useFetch('/api/users', {
query: { page: 1, limit: 10 }
})
// With key for deduplication
const { data } = await useFetch(`/api/users/${userId}`, {
key: `user-${userId}`
})
// Lazy fetch (doesn't block navigation)
const { data } = await useLazyFetch('/api/users')
// Watch and refetch
const page = ref(1)
const { data } = await useFetch('/api/users', {
query: { page },
watch: [page]
})
// Cancel requests with AbortController signal (Nuxt 4.2+)
const controller = new AbortController()
const { data } = await useFetch('/api/users', {
signal: controller.signal
})
// Later: controller.abort() to cancel the request
// Manual cancellation via execute/refresh
const { data, execute } = await useFetch('/api/users', { immediate: false })
const abortController = new AbortController()
await execute({ signal: abortController.signal })
// Later: abortController.abort() to canceluseAsyncData()
// Custom async logic
const { data, error, pending, refresh } = await useAsyncData('users', async () => {
const response = await $fetch('/api/users')
return response.filter(u => u.active)
})
// Lazy version
const { data } = await useLazyAsyncData('users', async () => {
return await $fetch('/api/users')
})
// Cancel with AbortController (Nuxt 4.2+)
const controller = new AbortController()
const { data } = await useAsyncData('users', async () => {
return await $fetch('/api/users', { signal: controller.signal })
})
// Later: controller.abort() to cancel
// Custom cache logic with getCachedData
const { data } = await useAsyncData('users',
async () => $fetch('/api/users'),
{
getCachedData: (key) => {
// Return cached data or null/undefined to trigger fetch
const cached = useNuxtData(key)
return cached.data.value
}
}
)
// Deep reactivity for nested objects
// Default is shallow in Nuxt 4 (was deep in Nuxt 3)
const { data } = await useAsyncData('user',
async () => $fetch('/api/user'),
{
deep: true // Makes nested properties reactive
}
)
// Deduplication strategies (Nuxt 4.2+)
const { data } = await useAsyncData('users',
async () => $fetch('/api/users'),
{
dedupe: 'cancel' // Cancel existing requests when new one starts
// dedupe: 'defer' // Prevent new requests while one is pending
}
)
// Manual cancellation via execute/refresh
const { data, execute } = await useAsyncData('users',
async ({ signal }) => $fetch('/api/users', { signal }),
{ immediate: false }
)
const abortController = new AbortController()
await execute({ signal: abortController.signal })
// Later: abortController.abort() to cancelState Management
useState()
// Create shared state
const counter = useState('counter', () => 0)
// Use in components
counter.value++
// With type
const user = useState<User | null>('user', () => null)App Context
useNuxtApp()
const nuxtApp = useNuxtApp()
// Access provided values
const { $api, $hello } = nuxtApp
// Access hooks
nuxtApp.hook('page:finish', () => {
console.log('Page loaded')
})
// Access Vue app
nuxtApp.vueApp.use(SomePlugin)useRuntimeConfig()
// Access runtime config
const config = useRuntimeConfig()
// Public config (client + server)
const apiBase = config.public.apiBase
// Private config (server only)
const apiSecret = config.apiSecret // undefined on clientHead Management
useHead()
// Set page meta
useHead({
title: 'User Profile',
meta: [
{ name: 'description', content: 'View user profile' },
{ property: 'og:title', content: 'User Profile' }
],
link: [
{ rel: 'canonical', href: 'https://example.com/profile' }
]
})
// Dynamic values
const user = ref({ name: 'John' })
useHead({
title: () => `${user.value.name}'s Profile`
})useSeoMeta()
// Cleaner SEO meta
useSeoMeta({
title: 'User Profile',
description: 'View user profile',
ogTitle: 'User Profile',
ogDescription: 'View user profile',
ogImage: 'https://example.com/image.jpg',
twitterCard: 'summary_large_image'
})Best Practices
- Use useRequestURL() NOT window.origin/location
- Type routes with useRoute('/path/[param]')
- Use useFetch for API calls (deduplication, SSR)
- Key your fetches for proper caching
- useState for shared state across components
- useSeoMeta for cleaner SEO tags
Common Mistakes
| ❌ Wrong | ✅ Right |
|---|---|
window.origin | useRequestURL().origin |
window.location.pathname | useRequestURL().pathname |
fetch() in components | useFetch() or useAsyncData() |
router.push('/path/' + id) | router.push({ name: '/path/[id]', params: { id } }) |
| Duplicate fetches | Use key parameter |
Resources
- Nuxt composables: https://nuxt.com/docs/api/composables/use-fetch
- Data fetching: https://nuxt.com/docs/getting-started/data-fetching
- useRequestURL: https://nuxt.com/docs/api/composables/use-request-url
- For NuxtTime, NuxtLink, NuxtImg: See nuxt-components.md
Nuxt Configuration
When to Use
Configuring nuxt.config.ts, modules, auto-imports, runtime config, layers.
Basic Structure
// nuxt.config.ts
export default defineNuxtConfig({
devtools: { enabled: true },
modules: [
'@nuxtjs/tailwindcss',
'@pinia/nuxt'
],
runtimeConfig: {
// Private (server-only)
apiSecret: process.env.API_SECRET,
public: {
// Public (client + server)
apiBase: process.env.API_BASE || 'http://localhost:3000'
}
},
app: {
head: {
title: 'My App',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
]
}
}
})Runtime Config
Access runtime config in app:
// Server-side
const config = useRuntimeConfig()
console.log(config.apiSecret) // Available
// Client-side
const config = useRuntimeConfig()
console.log(config.public.apiBase) // Available
console.log(config.apiSecret) // undefined (private)Runtime Config Validation (Recommended)
Use nuxt-safe-runtime-config for type-safe runtime config with build-time validation:
npx nuxi module add nuxt-safe-runtime-configBenefits:
- Build-time validation (catches missing env vars early)
- Optional runtime validation (validates when server starts)
- Auto-generated types (no manual type definitions needed)
- No manual env var checks required (schema handles validation)
Example with Valibot:
import { number, object, optional, string } from 'valibot'
export default defineNuxtConfig({
modules: ['nuxt-safe-runtime-config'],
runtimeConfig: {
databaseUrl: process.env.DATABASE_URL,
secretKey: process.env.SECRET_KEY,
port: Number.parseInt(process.env.PORT || '3000'),
public: {
apiBase: process.env.PUBLIC_API_BASE,
appName: 'My App',
},
},
safeRuntimeConfig: {
$schema: object({
public: object({
apiBase: string(),
appName: optional(string()),
}),
databaseUrl: string(),
secretKey: string(),
port: optional(number()),
}),
validateAtRuntime: true, // Optional: validate when server starts
},
})Usage:
// Auto-typed from schema - no generics needed
const config = useSafeRuntimeConfig()
// config.public.apiBase is string
// config.databaseUrl is stringNo manual env checks needed:
// ❌ Don't do this with nuxt-safe-runtime-config
if (!config.databaseUrl) throw new Error('Missing DATABASE_URL')
// ✅ Schema validation handles it automatically
// If env var is missing, build fails with detailed errorWorks with Zod, ArkType, or any Standard Schema library. See: https://github.com/onmax/nuxt-safe-runtime-config
Auto-Imports
Nuxt auto-imports from these directories:
components/- Vue componentscomposables/- Composition functionsutils/- Utility functionsserver/utils/- Server utilities (server-only)
Custom Auto-Imports
export default defineNuxtConfig({
imports: {
dirs: [
'stores',
'types'
]
}
})Disable Auto-Import
export default defineNuxtConfig({
imports: {
autoImport: false
}
})Modules
export default defineNuxtConfig({
modules: [
'@nuxtjs/tailwindcss',
'@pinia/nuxt',
'@vueuse/nuxt',
['@nuxtjs/google-fonts', {
families: {
Inter: [400, 700]
}
}]
]
})App Config
For non-sensitive config exposed to client:
// app.config.ts
export default defineAppConfig({
theme: {
primaryColor: '#3b82f6',
borderRadius: '0.5rem'
}
})Access in app:
const appConfig = useAppConfig()
console.log(appConfig.theme.primaryColor)TypeScript
export default defineNuxtConfig({
typescript: {
strict: true,
typeCheck: true,
shim: false
}
})Build Configuration
export default defineNuxtConfig({
build: {
transpile: ['some-package']
},
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: '@use "@/assets/styles/variables" as *;'
}
}
}
}
})Route Rules
Pre-render, cache, or customize routes:
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/api/**': { cors: true },
'/admin/**': { ssr: false },
'/blog/**': { swr: 3600 } // Cache for 1 hour
}
})ISR Route Rules
Use isr for incremental static regeneration:
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // Static at build time
'/**': { isr: 60 }, // Regenerate every 60s
'/package/**': { isr: 60 }, // ISR for dynamic routes
'/search': { isr: false, cache: false }, // No cache
}
})Route Rule Layouts (Nuxt 4.3+)
Apply layouts via route rules for centralized layout management:
export default defineNuxtConfig({
routeRules: {
'/admin/**': { appLayout: 'admin' },
'/docs/**': { appLayout: 'docs' },
'/': { appLayout: 'default' }
}
})Benefits: Centralized layout control, no need for setPageLayout() in every page.
Inline Modules
Add conditional logic during nuxt prepare:
export default defineNuxtConfig({
modules: [
// Inline function module
function (_, nuxt) {
if (nuxt.options._prepare) {
// Disable expensive operations during prepare
nuxt.options.pwa ||= {}
nuxt.options.pwa.pwaAssets ||= { disabled: true }
}
},
'@nuxtjs/tailwindcss',
]
})Provider-Specific Modules
Use std-env to detect platform and configure accordingly:
// modules/vercel-cache.ts
import { defineNuxtModule } from 'nuxt/kit'
import { provider } from 'std-env'
export default defineNuxtModule({
meta: { name: 'vercel-cache' },
setup(_, nuxt) {
if (provider !== 'vercel') return
nuxt.hook('nitro:config', (nitroConfig) => {
nitroConfig.storage ||= {}
nitroConfig.storage.cache = {
driver: 'vercel-runtime-cache',
...nitroConfig.storage.cache,
}
})
}
})Then register in nuxt.config.ts:
export default defineNuxtConfig({
modules: ['~/modules/vercel-cache']
})Experimental Features
export default defineNuxtConfig({
future: {
compatibilityVersion: 4
},
experimental: {
typedPages: true,
viewTransition: true,
payloadExtraction: true // Enable ISR/SWR payload extraction (Nuxt 4.3+)
}
})Payload extraction (Nuxt 4.3+): Enables cached payloads during client navigation for ISR/SWR routes, improving performance.
Nitro Config
Server engine configuration:
export default defineNuxtConfig({
nitro: {
preset: 'vercel',
compressPublicAssets: true,
routeRules: {
'/api/**': { cors: true }
}
}
})Layers
Extend or share configuration:
export default defineNuxtConfig({
extends: [
'./base-layer'
]
})Environment Variables
Use .env file:
API_SECRET=secret123
API_BASE=https://api.example.comAccess via runtimeConfig:
export default defineNuxtConfig({
runtimeConfig: {
apiSecret: process.env.API_SECRET,
public: {
apiBase: process.env.API_BASE
}
}
})Best Practices
- Use nuxt-safe-runtime-config for runtime config with validation
- Public vs private - keep secrets in private runtimeConfig
- App config for non-sensitive client config
- Route rules for performance (prerender, cache, SWR)
- Auto-imports for cleaner code
- TypeScript strict mode for better DX
Common Mistakes
| ❌ Wrong | ✅ Right |
|---|---|
| Hardcoded API URLs | Use runtimeConfig.public |
| Secrets in app.config | Use runtimeConfig (private) |
| Import everything manually | Let Nuxt auto-import |
| process.env in client code | Use useRuntimeConfig() |
| Manual env var validation | Use nuxt-safe-runtime-config |
| if (!config.x) throw error | Schema validation handles it |
Resources
- Nuxt config: https://nuxt.com/docs/api/nuxt-config
- Runtime config: https://nuxt.com/docs/guide/going-further/runtime-config
- App config: https://nuxt.com/docs/guide/directory-structure/app-config
- Modules: https://nuxt.com/modules
Project Setup
Standard patterns for new Nuxt projects: CI, ESLint, package scripts.
CI Workflow
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: {node-version: 22, cache: pnpm}
- run: pnpm install --frozen-lockfile
- run: pnpm prepare
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test # if tests existWith env vars:
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test
API_KEY: testESLint Config
// eslint.config.mjs
import antfu from '@antfu/eslint-config'
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
antfu({
formatters: true,
vue: true,
pnpm: true,
ignores: ['.eslintcache', 'cache/**', '.claude/**', 'README.md', 'docs/**'],
}),
)For monorepos, add:
ignores: ['apps/web/.nuxt/**', 'packages/**/dist/**']Package Scripts
{
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"preview": "nuxt preview",
"prepare": "nuxt prepare",
"lint": "eslint . --cache",
"lint:fix": "eslint . --fix --cache",
"typecheck": "nuxt typecheck"
}
}Key Conventions
| Convention | Standard |
|---|---|
| Package manager | pnpm with --frozen-lockfile in CI |
| Node version | 22-24 |
| ESLint base | @antfu/eslint-config |
| Formatter | Via ESLint (formatters: true), no separate Prettier |
| Cache | --cache flag on lint scripts |
| Prepare step | Required before lint/typecheck in CI |
NuxtHub Deployment
# .github/workflows/nuxthub.yml
name: Deploy to NuxtHub
on: push
jobs:
deploy:
runs-on: ubuntu-latest
permissions: {contents: read, id-token: write}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: {node-version: 22, cache: pnpm}
- run: pnpm install
- uses: nuxt-hub/action@v2
with:
project-key: your-project-keyFor pnpm catalogs, release workflows, tsconfig patterns: see ts-library skillNuxt File-Based Routing
When to Use
Working with pages/ or layouts/ directories, file-based routing, navigation.
File-Based Routing Basics
pages/ folder structure directly maps to routes. File names determine URLs.
Naming Conventions
Key principles:
- ALWAYS use descriptive params:
[userId].vueNOT[id].vue - Optional params:
[[paramName]].vue - Catch-all:
[...path].vue - Route groups for organization:
(folder)/groups files without affecting URLs
Red Flags - Stop and Check Skill
If you're thinking any of these, STOP and re-read this skill:
- "String paths are simpler than typed routes"
- "Generic param names like [id] are fine"
- "I remember how Nuxt 3 worked"
All of these mean: You're about to use outdated patterns. Use Nuxt 4 patterns instead.
File Structure Example
pages/
├── index.vue # /
├── about.vue # /about
├── [...slug].vue # catch-all for 404
├── users.vue # parent route (layout for /users/*)
└── users/
├── index.vue # /users
└── [userId].vue # /users/:userIdRoute Groups for Organization
Route groups organize files WITHOUT affecting URLs. Wrap folder names in parentheses:
pages/
├── (marketing)/ # group folder (ignored in URL)
│ ├── about.vue # /about (not /marketing/about)
│ └── pricing.vue # /pricing
└── (admin)/ # group folder (ignored in URL)
├── dashboard.vue # /dashboard
└── settings.vue # /settingsUse route groups to:
- Organize pages by feature/team
- Group related routes without affecting URLs
- Keep large projects maintainable
- Apply middleware to specific groups (via
route.meta.groups)
Access route groups in middleware:
// middleware/auth.global.ts
export default defineNuxtRouteMiddleware((to) => {
// Check if route is in admin group
if (to.meta.groups?.includes('admin')) {
const auth = useAuthStore()
if (!auth.isAdmin) return navigateTo('/')
}
})Parent Routes (Layouts)
Parent route = layout for nested routes:
<!-- pages/users.vue -->
<template>
<div class="users-layout">
<nav>
<NuxtLink to="/users">All Users</NuxtLink>
<NuxtLink to="/users/create">Create User</NuxtLink>
</nav>
<NuxtPage />
</div>
</template>Child routes:
pages/
├── users.vue # Parent route with <NuxtPage />
└── users/
├── index.vue # /users
├── [userId].vue # /users/:userId
└── create.vue # /users/createdefinePage() for Route Customization
<script setup lang="ts">
definePage({
name: 'user-profile',
path: '/profile/:userId', // Override default path
alias: ['/me', '/profile'],
meta: {
requiresAuth: true,
title: 'User Profile',
roles: ['user', 'admin']
}
})
</script>
<template>
<div>Profile content</div>
</template>Typed Router
ALWAYS use typed routes for navigation:
// ✅ Type-safe with route name
await navigateTo({ name: '/users/[userId]', params: { userId: '123' } })
// ❌ String-based (not type-safe, avoid)
await navigateTo('/users/123')REQUIRED: Check `typed-router.d.ts` for available route names and params before navigating.
useRoute with Types
Pass route name for stricter typing:
// Generic route
const route = useRoute()
// Typed route (preferred)
const route = useRoute('/users/[userId]')
// route.params.userId is now typed correctlyNavigation
// Navigate to route
await navigateTo('/about')
await navigateTo({ name: '/users/[userId]', params: { userId: '123' } })
// Navigate with query
await navigateTo({ path: '/search', query: { q: 'nuxt' } })
// External redirect
await navigateTo('https://nuxt.com', { external: true })
// Replace history
await navigateTo('/login', { replace: true })
// Open in new tab
await navigateTo('/docs', { open: { target: '_blank' } })Route Meta & Middleware
<script setup lang="ts">
definePageMeta({
middleware: ['auth', 'admin'],
layout: 'dashboard',
meta: {
requiresAuth: true
}
})
</script>Dynamic Layout Switching
Use setPageLayout() to switch layouts programmatically:
<script setup lang="ts">
const user = useUser()
// Switch layout based on auth state
if (!user.value) {
setPageLayout('guest')
} else {
setPageLayout('dashboard')
}
// With layout props (Nuxt 4.3+)
setPageLayout('dashboard', {
sidebar: 'collapsed',
theme: 'dark'
})
</script>Dynamic Routes Patterns
[userId].vue # /users/123
[[slug]].vue # /blog or /blog/post (optional)
[...path].vue # /a/b/c (catch-all)
[[...path]].vue # / or /a/b/c (optional catch-all)Best Practices
- `index.vue` for index routes - valid and correct for creating default routes
- Route groups `(folder)/` for organization - group files without affecting URLs
- Descriptive param names -
[userId]not[id],[postSlug]not[slug] - Type-safe navigation - use route names, not strings
- Check typed-router.d.ts for available routes
- Parent routes for layouts -
users.vuewith<NuxtPage /> - Use definePage for custom paths/aliases
- Catch-all for 404 -
[...path].vueor[...slug].vue
Common Mistakes
| ❌ Wrong | ✅ Right |
|---|---|
[id].vue | [userId].vue or [postId].vue |
navigateTo('/users/' + id) | navigateTo({ name: '/users/[userId]', params: { userId: id } }) |
<Nuxt /> | <NuxtPage /> |
| Separate layouts/ folder | Parent routes with <NuxtPage /> |
Resources
- Nuxt routing: https://nuxt.com/docs/guide/directory-structure/pages
- File-based routing: https://nuxt.com/docs/getting-started/routing
Nuxt Server Patterns
Versions: Nuxt uses h3 v1 and nitropack v2. Patterns from h3 v2 or nitro v3 docs won't work.
When to Use
Working with server/ directory - API routes, server middleware, server utilities.
Server Directory Structure
server/
├── api/ # API endpoints
│ ├── users.get.ts # GET /api/users
│ ├── users.post.ts # POST /api/users
│ └── users/
│ └── [id].get.ts # GET /api/users/:id
├── routes/ # Non-API routes
│ └── healthz.get.ts # GET /healthz
├── middleware/ # Server middleware
│ └── log.ts
└── utils/ # Server utilities (auto-imported)
└── db.tsAPI Routes
File naming determines HTTP method and route:
users.get.ts→ GET /api/usersusers.post.ts→ POST /api/usersusers/[userId].get.ts→ GET /api/users/:userIdusers/[userId].delete.ts→ DELETE /api/users/:userId
REQUIRED: Use descriptive param names: [userId].get.ts NOT [id].get.ts
Red Flags - Stop and Check Skill
If you're thinking any of these, STOP and re-read this skill:
- "I'll use event.context.params like before"
- "Generic [id] is fine for params"
- "Don't need .get.ts suffix"
- "I remember how Nuxt 3 API routes worked"
All of these mean: You're using outdated patterns. Use Nuxt 4 patterns instead.
Basic API Route
// server/api/users.get.ts
export default defineEventHandler(async (event) => {
const users = await fetchUsers()
return users
})Route with Params
// server/api/users/[userId].get.ts
export default defineEventHandler(async (event) => {
const userId = getRouterParam(event, 'userId')
if (!userId) {
throw createError({
statusCode: 400,
message: 'User ID is required'
})
}
const user = await fetchUserById(userId)
if (!user) {
throw createError({
statusCode: 404,
message: 'User not found'
})
}
return user
})Route with Query Params
// server/api/users.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 10
const users = await fetchUsers({ page, limit })
return users
})Route with Body
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Validate body
if (!body.name || !body.email) {
throw createError({
statusCode: 400,
message: 'Missing required fields: name, email'
})
}
const user = await createUser(body)
setResponseStatus(event, 201)
return user
})Validation with Valibot
Use readValidatedBody and getValidatedQuery for schema validation:
// server/api/users.post.ts
import * as v from 'valibot'
const UserSchema = v.object({
name: v.pipe(v.string(), v.minLength(1)),
email: v.pipe(v.string(), v.email())
})
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, v.parser(UserSchema))
// body is typed as { name: string, email: string }
const user = await createUser(body)
setResponseStatus(event, 201)
return user
})// server/api/users.get.ts
import * as v from 'valibot'
const QuerySchema = v.object({
page: v.optional(v.pipe(v.string(), v.transform(Number)), '1'),
limit: v.optional(v.pipe(v.string(), v.transform(Number)), '10')
})
export default defineEventHandler(async (event) => {
const { page, limit } = await getValidatedQuery(event, v.parser(QuerySchema))
return fetchUsers({ page, limit })
})Error Handling
Use createError for HTTP errors:
throw createError({
statusCode: 400,
statusMessage: 'Bad Request',
message: 'Invalid input',
data: { field: 'email' } // Optional additional data
})Server Middleware
Runs on every server request:
// server/middleware/log.ts
export default defineEventHandler((event) => {
console.log(`${event.method} ${event.path}`)
})Named middleware for specific patterns:
// server/middleware/auth.ts
export default defineEventHandler((event) => {
const token = getRequestHeader(event, 'authorization')
if (!token) {
throw createError({
statusCode: 401,
message: 'Unauthorized'
})
}
// Attach user to event context
event.context.user = await verifyToken(token)
})Server Utils
Reusable server functions (auto-imported):
// server/utils/db.ts
import { db } from './database'
export async function fetchUsers(options: { page: number, limit: number }) {
return await db.select().from('users').limit(options.limit).offset((options.page - 1) * options.limit)
}
export async function fetchUserById(id: string) {
return await db.select().from('users').where({ id }).first()
}Auto-imported in all server routes and middleware.
Import server utils from client (Nuxt 4.3+):
// Use #server alias for type-safe server-only imports
import type { User } from '#server/utils/db'Note: Only types are imported; actual server code never bundles into client.
Cached Functions
Use defineCachedFunction for caching expensive operations in server utils:
// server/utils/github.ts
export const fetchRepo = defineCachedFunction(
async (owner: string, repo: string) => {
return await $fetch(`https://api.github.com/repos/${owner}/${repo}`)
},
{
maxAge: 60 * 5, // Cache for 5 minutes
swr: true, // Stale-while-revalidate
name: 'github-repo',
getKey: (owner, repo) => `${owner}/${repo}`,
}
)Cached Event Handlers
Use defineCachedEventHandler for ISR-style caching on API routes:
// server/api/products/[productId].get.ts
export default defineCachedEventHandler(
async (event) => {
const productId = getRouterParam(event, 'productId')
return await fetchProductById(productId)
},
{
maxAge: 3600, // Cache for 1 hour
swr: true, // Serve stale while revalidating
getKey: event => getRouterParam(event, 'productId') ?? '',
}
)Generic Error Handler
Centralize error handling for H3 errors, validation errors, and fallbacks:
// server/utils/error-handler.ts
import { isError, createError } from 'h3'
import * as v from 'valibot'
export function handleApiError(error: unknown, fallback: { statusCode?: number, message: string }): never {
// Re-throw existing H3 errors
if (isError(error)) throw error
// Handle Valibot validation errors
if (v.isValiError(error)) {
throw createError({ statusCode: 400, message: error.issues[0].message })
}
// Generic fallback
throw createError({ statusCode: fallback.statusCode ?? 502, message: fallback.message })
}Usage in routes:
export default defineEventHandler(async (event) => {
try {
const data = await fetchExternalApi()
return data
} catch (error) {
handleApiError(error, { statusCode: 502, message: 'Failed to fetch data' })
}
})Request Helpers
// Get params
const userId = getRouterParam(event, 'userId')
// Get query
const query = getQuery(event)
// Get body
const body = await readBody(event)
// Get headers
const auth = getRequestHeader(event, 'authorization')
// Get cookies
const token = getCookie(event, 'token')
// Get method
const method = getMethod(event)
// Get IP
const ip = getRequestIP(event)Response Helpers
// Set status code
setResponseStatus(event, 201)
// Set headers
setResponseHeader(event, 'X-Custom', 'value')
setResponseHeaders(event, { 'X-Custom': 'value', 'X-Another': 'value' })
// Set cookies
setCookie(event, 'token', 'value', {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 1 week
})
// Redirect
return sendRedirect(event, '/login', 302)
// Stream
return sendStream(event, stream)
// No content
return sendNoContent(event)Background Tasks
Use event.waitUntil() for async tasks that shouldn't block the response (Nuxt 4+):
// server/api/analytics.post.ts
export default defineEventHandler(async (event) => {
const data = await readBody(event)
// Don't block response with analytics logging
event.waitUntil(
logAnalytics(data)
)
return { success: true }
})Use cases: logging, caching, background processing, async cleanup.
Best Practices
- Use descriptive param names -
[userId]not[id] - Keep routes thin - delegate to server utils
- Validate input at route level
- Use typed errors with createError
- Handle errors gracefully - don't expose internals
- Use server utils for DB/external APIs
- Don't expose sensitive data in responses
- Set proper status codes - 201 for created, 204 for no content
- Use event.waitUntil() for background tasks that shouldn't block responses
Common Mistakes
| ❌ Wrong | ✅ Right |
|---|---|
event.context.params.id | getRouterParam(event, 'id') |
return res.json(data) | return data |
[id].get.ts | [userId].get.ts |
users-id.get.ts | users/[id].get.ts |
| Throw generic errors | Use createError with status |
WebSocket
// server/routes/_ws.ts
export default defineWebSocketHandler({
open(peer) {
console.log('Client connected:', peer.id)
},
message(peer, message) {
peer.send(`Echo: ${message.text()}`)
// Broadcast to all: peer.publish('channel', message)
},
close(peer) {
console.log('Client disconnected:', peer.id)
}
})Enable in config:
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
experimental: { websocket: true }
}
})Server-Sent Events (Experimental)
// server/api/stream.get.ts
export default defineEventHandler(async (event) => {
const stream = createEventStream(event)
const interval = setInterval(async () => {
await stream.push({ data: JSON.stringify({ time: Date.now() }) })
}, 1000)
stream.onClosed(() => {
clearInterval(interval)
})
return stream.send()
})Resources
- Nuxt server: https://nuxt.com/docs/guide/directory-structure/server
- h3 (Nitro engine): https://v1.h3.dev/
- Nitro: https://nitro.build/
For database/storage APIs: see nuxthub skillRelated skills
How it compares
Pick the nuxt skill over generic Vue guidance when agents must emit Nitro server routes, Nuxt middleware, and 4.x composable imports correctly.
FAQ
What changed from Nuxt 3 to Nuxt 4?
Use NuxtPage instead of Nuxt, getRouterParam on the server instead of context.params, and useRequestURL for origin instead of window.origin.
Should I load all reference files?
No, load only files relevant to the current task such as server.md for API endpoints or routing.md for pages.
How are server routes validated?
Use defineEventHandler with getValidatedQuery or similar h3 helpers and Zod schemas as shown in the quick start hello.get.ts example.
Is Nuxt safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.