| name | nuxt-server |
| description | | Nuxt 4 server-side development with Nitro: API routes, server middleware, database integration, and backend patterns. Use when: creating server API routes, implementing server middleware, integrating databases (D1, PostgreSQL, Drizzle), handling file uploads, implementing WebSockets, or building backend logic with Nitro. |
| license | MIT |
| metadata | {"version":"4.0.0","author":"Claude Skills Maintainers","category":"Framework","framework":"Nuxt","framework-version":"4.x","last-verified":"2025-12-28T00:00:00.000Z","keywords":["server routes","API routes","Nitro","defineEventHandler","getRouterParam","getQuery","readBody","setCookie","createError","server middleware","D1","Drizzle","PostgreSQL","WebSocket","file upload"]} |
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.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
- Understanding Nitro internals
Load references/database-patterns.md when:
- Integrating Cloudflare D1 with Drizzle
- Setting up PostgreSQL connections
- Implementing database migrations
- Building query patterns
Load references/websocket-patterns.md when:
- Implementing real-time features
- Building WebSocket endpoints
- Using Durable Objects for state
Basic Event Handler
export default defineEventHandler(async (event) => {
return {
users: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]
}
})
Request Utilities
URL Parameters
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
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 createError({
statusCode: 400,
message: 'Name and email are required'
})
}
return { success: true, user: { id: 1, ...body } }
})
Headers
export default defineEventHandler(async (event) => {
const authHeader = getHeader(event, 'authorization')
const contentType = getHeader(event, 'content-type')
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) => {
setResponseStatus(event, 201)
return { message: 'Resource created' }
})
Redirects
export default defineEventHandler(async (event) => {
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) => {
const sessionId = getCookie(event, 'session_id')
setCookie(event, 'session_id', 'abc123', {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7
})
deleteCookie(event, 'old_cookie')
return { sessionId }
})
Server Middleware
export default defineEventHandler(async (event) => {
const publicRoutes = ['/api/auth/login', '/api/health']
if (publicRoutes.includes(event.path)) {
return
}
const token = getHeader(event, 'authorization')?.replace('Bearer ', '')
if (!token) {
throw createError({
statusCode: 401,
message: 'Authentication required'
})
}
const user = await verifyToken(token)
event.context.user = user
})
Accessing Context in Routes
export default defineEventHandler(async (event) => {
const user = event.context.user
if (!user) {
throw createError({ statusCode: 401, message: 'Not authenticated' })
}
return { user }
})
Database Integration
Cloudflare D1 with Drizzle
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 })
}
export default defineEventHandler(async (event) => {
const db = useDB(event)
const users = await db.select().from(schema.users).limit(10)
return { users }
})
Schema Definition
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').(),
: (),
: (, { : })
.()
.$defaultFn( ())
})
CRUD Operations
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)
const [user] = await db.insert(users)
.values({ name: body.name, email: body.email })
.returning()
return { user }
})
export default defineEventHandler(async (event) => {
const db = useDB(event)
const id = getRouterParam(event, 'id')
const body = await readBody(event)
const [user] = await db.update(users)
.set({ name: body.name })
.where(eq(users.id, (id)))
.()
(!user) {
({ : , : })
}
{ user }
})
( (event) => {
db = (event)
id = (event, )
db.(users).((users., (id)))
{ : }
})
Validation with Zod
import { z } from 'zod'
const createUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.email(),
age: z.number().int().min(0).max(150).optional()
})
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const result = createUserSchema.safeParse(body)
if (!result.success) {
throw createError({
statusCode: 400,
message: 'Validation failed',
data: result.error.flatten()
})
}
const { name, email, age } = result.data
return { success: true }
})
File Uploads
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' })
}
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. }
})
Server Utilities
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
}
export default defineEventHandler(async (event) => {
const user = requireAuth(event)
const admin = requireRole(event, 'admin')
})
Common Anti-Patterns
Missing Method Suffix
Not Throwing Errors
export default defineEventHandler(async (event) => {
const user = await findUser(id)
if (!user) {
return { error: 'Not found' }
}
})
export default defineEventHandler(async (event) => {
const user = await findUser(id)
if (!user) {
throw createError({ statusCode: 404, message: 'Not found' })
}
})
Forgetting Async/Await
export default defineEventHandler((event) => {
const body = readBody(event)
})
export default defineEventHandler(async (event) => {
const body = await readBody(event)
})
Troubleshooting
404 on API Routes:
- Ensure file is in
server/api/ (not app/api/)
- Check method suffix matches request (
.get.ts for GET)
- Verify file extension is
.ts
Body is Empty:
- Ensure
await readBody(event) not 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
[[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: 4.0.0 | Last Updated: 2025-12-28 | License: MIT