Add nuxt-skills and update auto quotes to use new policy API structure

- Add nuxt-skills (vue, nuxt, nuxt-ui) to .claude/skills/
- Create useCustomerSelection() composable for managing insured/buyer selection
- Create usePolicyApi() composable for policy API operations
- Update auto quote components to use insured/buyer instead of client
- Update vehicle fields: remove valorVehiculo, add market_value, requested_value, rc_limits
- Make chassis_number and engine_number optional
- Update auto quote types and composables to match new API structure
- Update auto quote page to submit to policy API with new structure
This commit is contained in:
2026-04-27 14:56:53 -05:00
parent 67482f6629
commit a2eb1f3789
154 changed files with 10346 additions and 51 deletions

View File

@@ -0,0 +1,278 @@
# 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:**
```ts
// 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:
```ts
// middleware/admin.ts
export default defineNuxtRouteMiddleware((to, from) => {
const auth = useAuthStore()
if (!auth.isAdmin) {
return navigateTo('/')
}
})
```
Apply in page:
```vue
<script setup lang="ts">
definePageMeta({
middleware: ['admin']
})
</script>
```
### Middleware Return Values
```ts
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
```ts
// plugins/my-plugin.ts
export default defineNuxtPlugin((nuxtApp) => {
return {
provide: {
hello: (name: string) => `Hello ${name}!`
}
}
})
```
Use in components:
```vue
<script setup lang="ts">
const { $hello } = useNuxtApp()
console.log($hello('World')) // "Hello World!"
</script>
```
### Plugin with Vue Plugin
```ts
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
```ts
// 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
```ts
// 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.ts
```
### Async Plugins
```ts
// 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
```ts
// 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
```ts
// 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

View File

@@ -0,0 +1,162 @@
# 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:**
```vue
<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 object
- `external` - Force external link behavior
- `target` - Link target (`_blank`, etc.)
- `prefetch` - Enable/disable prefetching (default: true)
- `noPrefetch` - Disable prefetching
- `activeClass` - Class when route matches
- `exactActiveClass` - 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).
```vue
<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 path
- `alt` - Alt text (required for accessibility)
- `width` / `height` - Dimensions
- `sizes` - Responsive sizes
- `loading` - `lazy` (default) or `eager`
- `placeholder` - Show blur placeholder while loading
- `format` - 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:**
```vue
<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 timestamp
- `relative` - Show relative time ("2 hours ago")
- `locale` - Locale for formatting
- `year`, `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

View File

@@ -0,0 +1,323 @@
# 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`:**
```ts
// ✅ 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.pathname
```
**Why:** `window` is undefined during SSR. `useRequestURL()` works everywhere.
### useRequestURL() Patterns
```ts
// 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()
```ts
// 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 route
```
### useRouter()
```ts
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()
```ts
// 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.requiresAuth
```
## Data Fetching
### useFetch()
```ts
// 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 cancel
```
### useAsyncData()
```ts
// 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 cancel
```
## State Management
### useState()
```ts
// 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()
```ts
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()
```ts
// 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 client
```
## Head Management
### useHead()
```ts
// 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()
```ts
// 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

View File

@@ -0,0 +1,419 @@
# Nuxt Configuration
## When to Use
Configuring `nuxt.config.ts`, modules, auto-imports, runtime config, layers.
## Basic Structure
```ts
// 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:
```ts
// 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:
```bash
npx nuxi module add nuxt-safe-runtime-config
```
**Benefits:**
- 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:**
```ts
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:**
```ts
// Auto-typed from schema - no generics needed
const config = useSafeRuntimeConfig()
// config.public.apiBase is string
// config.databaseUrl is string
```
**No manual env checks needed:**
```ts
// ❌ 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 error
```
Works 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 components
- `composables/` - Composition functions
- `utils/` - Utility functions
- `server/utils/` - Server utilities (server-only)
### Custom Auto-Imports
```ts
export default defineNuxtConfig({
imports: {
dirs: [
'stores',
'types'
]
}
})
```
### Disable Auto-Import
```ts
export default defineNuxtConfig({
imports: {
autoImport: false
}
})
```
## Modules
```ts
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:
```ts
// app.config.ts
export default defineAppConfig({
theme: {
primaryColor: '#3b82f6',
borderRadius: '0.5rem'
}
})
```
Access in app:
```ts
const appConfig = useAppConfig()
console.log(appConfig.theme.primaryColor)
```
## TypeScript
```ts
export default defineNuxtConfig({
typescript: {
strict: true,
typeCheck: true,
shim: false
}
})
```
## Build Configuration
```ts
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:
```ts
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:
```ts
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:
```ts
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:
```ts
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:
```ts
// 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:
```ts
export default defineNuxtConfig({
modules: ['~/modules/vercel-cache']
})
```
## Experimental Features
```ts
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:
```ts
export default defineNuxtConfig({
nitro: {
preset: 'vercel',
compressPublicAssets: true,
routeRules: {
'/api/**': { cors: true }
}
}
})
```
## Layers
Extend or share configuration:
```ts
export default defineNuxtConfig({
extends: [
'./base-layer'
]
})
```
## Environment Variables
Use `.env` file:
```env
API_SECRET=secret123
API_BASE=https://api.example.com
```
Access via runtimeConfig:
```ts
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

View File

@@ -0,0 +1,107 @@
# Project Setup
Standard patterns for new Nuxt projects: CI, ESLint, package scripts.
## CI Workflow
```yaml
# .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 exist
```
**With env vars:**
```yaml
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test
API_KEY: test
```
## ESLint Config
```js
// 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:**
```js
ignores: ['apps/web/.nuxt/**', 'packages/**/dist/**']
```
## Package Scripts
```json
{
"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
```yaml
# .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-key
```
> **For pnpm catalogs, release workflows, tsconfig patterns:** see `ts-library` skill

View File

@@ -0,0 +1,242 @@
# Nuxt 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].vue` NOT `[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/:userId
```
## Route 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 # /settings
```
**Use 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:**
```ts
// 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:
```vue
<!-- 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/create
```
## definePage() for Route Customization
```vue
<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:**
```ts
// ✅ 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:
```ts
// Generic route
const route = useRoute()
// Typed route (preferred)
const route = useRoute('/users/[userId]')
// route.params.userId is now typed correctly
```
## Navigation
```ts
// 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
```vue
<script setup lang="ts">
definePageMeta({
middleware: ['auth', 'admin'],
layout: 'dashboard',
meta: {
requiresAuth: true
}
})
</script>
```
## Dynamic Layout Switching
Use `setPageLayout()` to switch layouts programmatically:
```vue
<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.vue` with `<NuxtPage />`
- **Use definePage** for custom paths/aliases
- **Catch-all for 404** - `[...path].vue` or `[...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

View File

@@ -0,0 +1,451 @@
# 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.ts
```
## API Routes
File naming determines HTTP method and route:
- `users.get.ts` → GET /api/users
- `users.post.ts` → POST /api/users
- `users/[userId].get.ts` → GET /api/users/:userId
- `users/[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
```ts
// server/api/users.get.ts
export default defineEventHandler(async (event) => {
const users = await fetchUsers()
return users
})
```
### Route with Params
```ts
// 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
```ts
// 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
```ts
// 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:
```ts
// 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
})
```
```ts
// 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:
```ts
throw createError({
statusCode: 400,
statusMessage: 'Bad Request',
message: 'Invalid input',
data: { field: 'email' } // Optional additional data
})
```
## Server Middleware
Runs on every server request:
```ts
// server/middleware/log.ts
export default defineEventHandler((event) => {
console.log(`${event.method} ${event.path}`)
})
```
Named middleware for specific patterns:
```ts
// 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):
```ts
// 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+):**
```ts
// 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:
```ts
// 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:
```ts
// 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:
```ts
// 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:
```ts
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
```ts
// 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
```ts
// 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+):
```ts
// 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
```ts
// 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:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
experimental: { websocket: true }
}
})
```
## Server-Sent Events (Experimental)
```ts
// 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` skill