Nuxt server
Skill secondsky/claude-skills/plugins/nuxt-v5/skills/nuxt-server
Production-ready skills for Claude Code CLI - Cloudflare, React, Tailwind v4, and AI integrations
npx -y skills add secondsky/claude-skills --skill nuxt-serverAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
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.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
12.3 KB, as published. Nobody here has run it
Nuxt 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 | nitropack | nitro |
| h3 imports | import { ... } from 'h3' | import { ... } from 'nitro/h3' |
| Error creation | createError({statusCode}) | new HTTPError({status}) |
| Event path | event.path | event.url.pathname |
| Event method | event.method | event.req.method |
| Status code | event.node.res.statusCode | event.res.status |
| Response headers | setResponseHeader(event, ...) | event.res.headers.set(...) |
| Runtime config | useRuntimeConfig(event) | useRuntimeConfig() |
| Route rules redirect | statusCode | status |
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.ts | GET |
.post.ts | POST |
.put.ts | PUT |
.patch.ts | PATCH |
.delete.ts | DELETE |
.ts | All methods |
When to Load References
Load references/server.md when:
- 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
import { ... } from 'h3'toimport { ... } from 'nitro/h3' - Auto-imports (
defineEventHandler,getQuery,readBody) continue to work
404 on API Routes:
- Ensure file is in
server/api/ - Check method suffix matches request (
.get.tsfor GET)
Body is Empty:
- Ensure
await readBody(event)notreadBody(event)
D1 Binding Not Found:
- Check wrangler.toml has
[[d1_databases]]configured - 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