Claude-skills nuxt-server
install
source · Clone the upstream repo
git clone https://github.com/secondsky/claude-skills
Claude Code · Install into ~/.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-v4/skills/nuxt-server" ~/.claude/skills/secondsky-claude-skills-nuxt-server && rm -rf "$T"
manifest:
plugins/nuxt-v4/skills/nuxt-server/SKILL.mdsource content
Nuxt 4 Server Development
Server routes, API patterns, and backend development with Nitro.
Quick Reference
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 │ └── auth.ts # Runs on every request ├── plugins/ # Nitro plugins │ └── database.ts # Initialize database └── utils/ # Server utilities └── db.ts # Database helpers
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
- Understanding Nitro internals
Load
when:references/database-patterns.md
- Integrating Cloudflare D1 with Drizzle
- Setting up PostgreSQL connections
- Implementing database migrations
- Building query patterns
Load
when:references/websocket-patterns.md
- Implementing real-time features
- Building WebSocket endpoints
- Using Durable Objects for state
Basic Event Handler
// server/api/users/index.get.ts export default defineEventHandler(async (event) => { // Return data (automatically serialized to JSON) return { users: [ { id: 1, name: 'John' }, { id: 2, name: 'Jane' } ] } })
Request Utilities
URL Parameters
// server/api/users/[id].get.ts export default defineEventHandler(async (event) => { const id = getRouterParam(event, 'id') if (!id) { throw createError({ statusCode: 400, message: 'User ID is required' }) } return { id } })
Query Parameters
// GET /api/users?page=1&limit=10&search=john 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
// server/api/users/index.post.ts export default defineEventHandler(async (event) => { const body = await readBody(event) // Validate body if (!body.name || !body.email) { throw createError({ statusCode: 400, message: 'Name and email are required' }) } // Create user... return { success: true, user: { id: 1, ...body } } })
Headers
export default defineEventHandler(async (event) => { // Read headers const authHeader = getHeader(event, 'authorization') const contentType = getHeader(event, 'content-type') // Set response headers setHeader(event, 'X-Custom-Header', 'value') setHeader(event, 'Cache-Control', 'max-age=3600') return { authHeader, contentType } })
Response Utilities
Setting Status Code
export default defineEventHandler(async (event) => { // Set status code setResponseStatus(event, 201) // Created return { message: 'Resource created' } })
Redirects
export default defineEventHandler(async (event) => { // Redirect return sendRedirect(event, '/new-location', 302) })
Error Handling
export default defineEventHandler(async (event) => { const id = getRouterParam(event, 'id') const user = await findUser(id) if (!user) { throw createError({ statusCode: 404, statusMessage: 'Not Found', message: `User with ID ${id} not found` }) } return user })
Cookies
export default defineEventHandler(async (event) => { // Read cookie const sessionId = getCookie(event, 'session_id') // Set cookie setCookie(event, 'session_id', 'abc123', { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 60 * 60 * 24 * 7 // 1 week }) // Delete cookie deleteCookie(event, 'old_cookie') return { sessionId } })
Server Middleware
// server/middleware/auth.ts export default defineEventHandler(async (event) => { // Skip for public routes const publicRoutes = ['/api/auth/login', '/api/health'] if (publicRoutes.includes(event.path)) { return // Continue to next handler } // Check authentication const token = getHeader(event, 'authorization')?.replace('Bearer ', '') if (!token) { throw createError({ statusCode: 401, message: 'Authentication required' }) } // Verify token and attach user to context const user = await verifyToken(token) event.context.user = user })
Accessing Context in Routes
// server/api/profile.get.ts export default defineEventHandler(async (event) => { // User attached by middleware const user = event.context.user if (!user) { throw createError({ statusCode: 401, message: 'Not authenticated' }) } return { user } })
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 } })
Schema Definition
// server/database/schema.ts import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core' export const users = sqliteTable('users', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull(), email: text('email').notNull().unique(), createdAt: integer('created_at', { mode: 'timestamp' }) .notNull() .$defaultFn(() => new Date()) }) export const posts = sqliteTable('posts', { id: integer('id').primaryKey({ autoIncrement: true }), userId: integer('user_id').notNull().references(() => users.id), title: text('title').notNull(), content: text('content'), createdAt: integer('created_at', { mode: 'timestamp' }) .notNull() .$defaultFn(() => new Date()) })
CRUD Operations
// server/api/users/index.post.ts import { users } from '~/server/database/schema' import { eq } from 'drizzle-orm' export default defineEventHandler(async (event) => { const db = useDB(event) const body = await readBody(event) // Create const [user] = await db.insert(users) .values({ name: body.name, email: body.email }) .returning() return { user } }) // server/api/users/[id].put.ts export default defineEventHandler(async (event) => { const db = useDB(event) const id = getRouterParam(event, 'id') const body = await readBody(event) // Update const [user] = await db.update(users) .set({ name: body.name }) .where(eq(users.id, Number(id))) .returning() if (!user) { throw createError({ statusCode: 404, message: 'User not found' }) } return { user } }) // server/api/users/[id].delete.ts export default defineEventHandler(async (event) => { const db = useDB(event) const id = getRouterParam(event, 'id') // Delete await db.delete(users).where(eq(users.id, Number(id))) return { success: true } })
Validation with Zod
// server/api/users/index.post.ts import { z } from 'zod' const createUserSchema = z.object({ name: z.string().min(2).max(100), email: z.string().email(), age: z.number().int().min(0).max(150).optional() }) export default defineEventHandler(async (event) => { const body = await readBody(event) // Validate const result = createUserSchema.safeParse(body) if (!result.success) { throw createError({ statusCode: 400, message: 'Validation failed', data: result.error.flatten() }) } // Use validated data const { name, email, age } = result.data // Create user... return { success: true } })
File Uploads
// server/api/upload.post.ts export default defineEventHandler(async (event) => { const formData = await readMultipartFormData(event) if (!formData) { throw createError({ statusCode: 400, message: 'No file uploaded' }) } const file = formData.find(f => f.name === 'file') if (!file) { throw createError({ statusCode: 400, message: 'File field is required' }) } // file.filename - Original filename // file.type - MIME type // file.data - Buffer with file contents // Upload to R2 (Cloudflare) 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 { H3Event } from 'h3' export function requireAuth(event: H3Event) { const user = event.context.user if (!user) { throw createError({ statusCode: 401, message: 'Authentication required' }) } return user } export function requireRole(event: H3Event, role: string) { const user = requireAuth(event) if (user.role !== role) { throw createError({ statusCode: 403, message: 'Insufficient permissions' }) } return user } // Usage in routes export default defineEventHandler(async (event) => { const user = requireAuth(event) // or const admin = requireRole(event, 'admin') })
Common Anti-Patterns
Missing Method Suffix
// WRONG - Handles all methods // server/api/users.ts // CORRECT - Explicit method // server/api/users.get.ts → GET // server/api/users.post.ts → POST
Not Throwing Errors
// WRONG - Returns error as data export default defineEventHandler(async (event) => { const user = await findUser(id) if (!user) { return { error: 'Not found' } // 200 status! } }) // CORRECT - Throw error export default defineEventHandler(async (event) => { const user = await findUser(id) if (!user) { throw createError({ statusCode: 404, message: 'Not found' }) } })
Forgetting Async/Await
// WRONG - Body not awaited export default defineEventHandler((event) => { const body = readBody(event) // Returns Promise! }) // CORRECT export default defineEventHandler(async (event) => { const body = await readBody(event) })
Troubleshooting
404 on API Routes:
- Ensure file is in
(notserver/api/
)app/api/ - Check method suffix matches request (
for GET).get.ts - Verify file extension is
.ts
Body is Empty:
- Ensure
notawait readBody(event)readBody(event) - Check Content-Type header is set correctly
- For multipart, use
readMultipartFormData
Middleware Not Running:
- Check file is in
server/middleware/ - Middleware runs for ALL requests unless filtered
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: 4.0.0 | Last Updated: 2025-12-28 | License: MIT