> 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.

fetch
$curl "https://skillshub.wtf/secondsky/claude-skills/nuxt-server-nuxt-v5?format=md"
SKILL.mdnuxt-server

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)

AreaNuxt 4 (Nitro v2)Nuxt 5 (Nitro v3)
Packagenitropacknitro
h3 importsimport { ... } from 'h3'import { ... } from 'nitro/h3'
Error creationcreateError({statusCode})new HTTPError({status})
Event pathevent.pathevent.url.pathname
Event methodevent.methodevent.req.method
Status codeevent.node.res.statusCodeevent.res.status
Response headerssetResponseHeader(event, ...)event.res.headers.set(...)
Runtime configuseRuntimeConfig(event)useRuntimeConfig()
Route rules redirectstatusCodestatus

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

SuffixHTTP Method
.get.tsGET
.post.tsPOST
.put.tsPUT
.patch.tsPATCH
.delete.tsDELETE
.tsAll 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' to import { ... } 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.ts for GET)

Body is Empty:

  • Ensure await readBody(event) not readBody(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

> related_skills --same-repo

> zustand-state-management

--- name: zustand-state-management description: Zustand state management for React with TypeScript. Use for global state, Redux/Context API migration, localStorage persistence, slices pattern, devtools, Next.js SSR, or encountering hydration errors, TypeScript inference issues, persist middleware problems, infinite render loops. Keywords: zustand, state management, React state, TypeScript state, persist middleware, devtools, slices pattern, global state, React hooks, create store, useBoundS

> zod

TypeScript-first schema validation and type inference. Use for validating API requests/responses, form data, env vars, configs, defining type-safe schemas with runtime validation, transforming data, generating JSON Schema for OpenAPI/AI, or encountering missing validation errors, type inference issues, validation error handling problems. Zero dependencies (2kb gzipped).

> xss-prevention

--- name: xss-prevention description: XSS attack prevention with input sanitization, output encoding, Content Security Policy. Use for user-generated content, rich text editors, web application security, or encountering stored XSS, reflected XSS, DOM manipulation, script injection errors. Keywords: sanitization, HTML-encoding, DOMPurify, CSP, Content-Security-Policy, rich-text-editor, user-input, escaping, innerHTML, DOM-manipulation, stored-XSS, reflected-XSS, input-validation, output-encodi

> wordpress-plugin-core

--- name: wordpress-plugin-core description: WordPress plugin development with hooks, security, REST API, custom post types. Use for plugin creation, $wpdb queries, Settings API, or encountering SQL injection, XSS, CSRF, nonce errors. Keywords: wordpress plugin development, wordpress security, wordpress hooks, wordpress filters, wordpress database, wpdb prepare, sanitize_text_field, esc_html, wp_nonce, custom post type, register_post_type, settings api, rest api, admin-ajax, wordpress sql inj

┌ stats

installs/wk0
░░░░░░░░░░
github stars100
██████████
first seenApr 3, 2026
└────────────