Claude-skills nuxt-server
Nuxt 5 server-side development with Nitro v3, h3 v2, API routes, middleware, and database integration. Use when creating server routes, integrating D1/Drizzle, or migrating from Nitro v2.
git clone https://github.com/secondsky/claude-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/secondsky/claude-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/nuxt-v5/skills/nuxt-server" ~/.claude/skills/secondsky-claude-skills-nuxt-server-d2a116 && rm -rf "$T"
plugins/nuxt-v5/skills/nuxt-server/SKILL.mdNuxt 5 Server Development
Server routes, API patterns, and backend development with Nitro v3.
Use when: creating server API routes, implementing server middleware, integrating databases (D1, PostgreSQL, Drizzle), handling file uploads, migrating from Nitro v2/h3 v1 to Nitro v3/h3 v2, or building backend logic.
Quick Reference
Nuxt 5 Server API Changes (from Nuxt 4)
| Area | Nuxt 4 (Nitro v2) | Nuxt 5 (Nitro v3) |
|---|---|---|
| Package | | |
| h3 imports | | |
| Error creation | | |
| Event path | | |
| Event method | | |
| Status code | | |
| Response headers | | |
| Runtime config | | |
| Route rules redirect | | |
File-Based Server Routes
server/ ├── api/ # API endpoints (/api/*) │ ├── users/ │ │ ├── index.get.ts → GET /api/users │ │ ├── index.post.ts → POST /api/users │ │ ├── [id].get.ts → GET /api/users/:id │ │ ├── [id].put.ts → PUT /api/users/:id │ │ └── [id].delete.ts → DELETE /api/users/:id │ └── health.get.ts → GET /api/health ├── routes/ # Non-API routes │ └── sitemap.xml.get.ts → GET /sitemap.xml ├── middleware/ # Server middleware ├── plugins/ # Nitro plugins └── utils/ # Server utilities
HTTP Method Suffixes
| Suffix | HTTP Method |
|---|---|
| GET |
| POST |
| PUT |
| PATCH |
| DELETE |
| All methods |
When to Load References
Load
when:references/server.md
- Implementing complex API routes
- Handling authentication and sessions
- Working with cookies and headers
- Building file upload endpoints
- Migrating from h3 v1 to h3 v2 API
Basic Event Handler
// server/api/users/index.get.ts export default defineEventHandler(async (event) => { return { users: [ { id: 1, name: 'John' }, { id: 2, name: 'Jane' } ] } })
Web Standard Event API (v5)
Request Properties
// Nuxt 5 uses Web Standard APIs export default defineEventHandler((event) => { // Path const path = event.url.pathname // was: event.path const search = event.url.search // URLSearchParams // Method const method = event.req.method // was: event.method // Headers (Web Headers API) const auth = event.req.headers.get('authorization') // was: getHeader(event, 'authorization') const allHeaders = event.req.headers return { path, search, method, auth } })
Response Properties
export default defineEventHandler((event) => { // Set status event.res.status = 201 // was: setResponseStatus(event, 201) // Set headers (Web Headers API) event.res.headers.set('X-Custom', 'value') // was: setHeader(event, 'X-Custom', 'value') event.res.headers.append('Set-Cookie', 'val') // was: appendResponseHeader(event, ...) return { message: 'Created' } })
Legacy Helpers Still Work
The h3 v1 helper functions (auto-imported) still work for request reading:
// These continue to work (auto-imported) const id = getRouterParam(event, 'id') const query = getQuery(event) const body = await readBody(event) const cookie = getCookie(event, 'name')
But for setting responses, prefer the Web Standard API:
// Prefer v5 style event.res.status = 201 event.res.headers.set('Cache-Control', 'max-age=3600') // Still works but deprecated for setting setResponseStatus(event, 201) setHeader(event, 'Cache-Control', 'max-age=3600')
Error Handling (v5 Change)
Server-Side: HTTPError
In server routes, use
HTTPError instead of createError:
// Nuxt 5 server routes import { HTTPError } from 'nitro/h3' export default defineEventHandler(async (event) => { const id = getRouterParam(event, 'id') if (!id) { throw new HTTPError({ status: 400, statusText: 'User ID is required' }) } const user = await findUser(id) if (!user) { throw new HTTPError({ status: 404, statusText: 'Not Found' }) } return user })
App-Side: createError (Unchanged)
In the Vue part of your app (
app/ directory), createError continues to work:
// app/ code - createError still works throw createError({ statusCode: 404, statusMessage: 'Page Not Found', fatal: true })
Validation Errors
import { z } from 'zod' import { HTTPError } from 'nitro/h3' const createUserSchema = z.object({ name: z.string().min(2).max(100), email: z.string().email() }) export default defineEventHandler(async (event) => { const body = await readBody(event) const result = createUserSchema.safeParse(body) if (!result.success) { throw new HTTPError({ status: 400, statusText: 'Validation failed', }) } return { success: true, data: result.data } })
Request Utilities
URL Parameters
// server/api/users/[id].get.ts export default defineEventHandler(async (event) => { const id = getRouterParam(event, 'id') if (!id) { throw new HTTPError({ status: 400, statusText: 'User ID is required' }) } return { id } })
Query Parameters
export default defineEventHandler(async (event) => { const query = getQuery(event) const page = Number(query.page) || 1 const limit = Number(query.limit) || 10 const search = query.search as string | undefined return { page, limit, search } })
Request Body
export default defineEventHandler(async (event) => { const body = await readBody(event) if (!body.name || !body.email) { throw new HTTPError({ status: 400, statusText: 'Name and email are required' }) } return { success: true, user: { id: 1, ...body } } })
Cookies
export default defineEventHandler(async (event) => { // Read cookie (helper still works) const sessionId = getCookie(event, 'session_id') // Set cookie setCookie(event, 'session_id', 'abc123', { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 60 * 60 * 24 * 7 }) // Delete cookie deleteCookie(event, 'old_cookie') return { sessionId } })
Server Middleware
// server/middleware/auth.ts export default defineEventHandler(async (event) => { const publicRoutes = ['/api/auth/login', '/api/health'] // v5: Use event.url.pathname instead of event.path if (publicRoutes.includes(event.url.pathname)) { return } const token = event.req.headers.get('authorization')?.replace('Bearer ', '') if (!token) { throw new HTTPError({ status: 401, statusText: 'Authentication required' }) } const user = await verifyToken(token) event.context.user = user })
Runtime Config (v5 Change)
// Nuxt 5: useRuntimeConfig() no longer accepts event export default defineEventHandler(() => { const config = useRuntimeConfig() return { apiBase: config.public.apiBase } }) // Nuxt 4 (deprecated in v5): // const config = useRuntimeConfig(event)
Database Integration
Cloudflare D1 with Drizzle
// server/utils/db.ts import { drizzle } from 'drizzle-orm/d1' import * as schema from '~/server/database/schema' export function useDB(event: H3Event) { const { DB } = event.context.cloudflare.env return drizzle(DB, { schema }) } // server/api/users/index.get.ts export default defineEventHandler(async (event) => { const db = useDB(event) const users = await db.select().from(schema.users).limit(10) return { users } })
CRUD Operations
// server/api/users/index.post.ts import { users } from '~/server/database/schema' export default defineEventHandler(async (event) => { const db = useDB(event) const body = await readBody(event) const [user] = await db.insert(users) .values({ name: body.name, email: body.email }) .returning() return { user } }) // server/api/users/[id].delete.ts import { eq } from 'drizzle-orm' import { users } from '~/server/database/schema' import { HTTPError } from 'nitro/h3' export default defineEventHandler(async (event) => { const db = useDB(event) const id = getRouterParam(event, 'id') if (!id) { throw new HTTPError({ status: 400, statusText: 'ID is required' }) } await db.delete(users).where(eq(users.id, Number(id))) return { success: true } })
File Uploads
// server/api/upload.post.ts import { HTTPError } from 'nitro/h3' export default defineEventHandler(async (event) => { const formData = await readMultipartFormData(event) if (!formData) { throw new HTTPError({ status: 400, statusText: 'No file uploaded' }) } const file = formData.find(f => f.name === 'file') if (!file) { throw new HTTPError({ status: 400, statusText: 'File field is required' }) } const { R2 } = event.context.cloudflare.env const key = `uploads/${Date.now()}-${file.filename}` await R2.put(key, file.data) return { key, filename: file.filename, type: file.type } })
Server Utilities
// server/utils/auth.ts import { HTTPError } from 'nitro/h3' export function requireAuth(event: H3Event) { const user = event.context.user if (!user) { throw new HTTPError({ status: 401, statusText: 'Authentication required' }) } return user } export function requireRole(event: H3Event, role: string) { const user = requireAuth(event) if (user.role !== role) { throw new HTTPError({ status: 403, statusText: 'Insufficient permissions' }) } return user }
Route Rules (v5 Change)
// nuxt.config.ts - redirect status property renamed export default defineNuxtConfig({ routeRules: { '/old-page': { redirect: { to: '/new-page', status: 302 } // was: statusCode }, '/api/**': { cors: true }, '/blog/**': { swr: 3600 } } })
Common Anti-Patterns
Using createError in Server Routes
// WRONG in v5 server routes - use HTTPError import { createError } from 'h3' throw createError({ statusCode: 404, statusMessage: 'Not found' }) // CORRECT in v5 server routes import { HTTPError } from 'nitro/h3' throw new HTTPError({ status: 404, statusText: 'Not found' })
Using event.path
// WRONG - deprecated in v5 const path = event.path // CORRECT const path = event.url.pathname
Using useRuntimeConfig(event)
// WRONG - no longer accepts event in v5 const config = useRuntimeConfig(event) // CORRECT const config = useRuntimeConfig()
Not Throwing Errors
// WRONG - Returns error as data with 200 status if (!user) { return { error: 'Not found' } } // CORRECT - Throw error if (!user) { throw new HTTPError({ status: 404, statusText: 'Not found' }) }
Troubleshooting
Import errors from 'h3':
- Change
toimport { ... } from 'h3'import { ... } from 'nitro/h3' - Auto-imports (
,defineEventHandler
,getQuery
) continue to workreadBody
404 on API Routes:
- Ensure file is in
server/api/ - Check method suffix matches request (
for GET).get.ts
Body is Empty:
- Ensure
notawait readBody(event)readBody(event)
D1 Binding Not Found:
- Check wrangler.toml has
configured[[d1_databases]] - Access via
event.context.cloudflare.env.DB
Related Skills
- nuxt-core: Project setup, routing, configuration
- nuxt-data: Composables, data fetching, state
- nuxt-production: Performance, testing, deployment
- cloudflare-d1: D1 database patterns
Version: 5.0.0 | Last Updated: 2026-03-30 | License: MIT