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:
451
.claude/skills/nuxt/references/server.md
Normal file
451
.claude/skills/nuxt/references/server.md
Normal 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
|
||||
Reference in New Issue
Block a user