Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Nuxt 4+ development patterns: server routes, file-based routing, middleware, composables, and h3 v1 / nitropack v2.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/server.md
1# Nuxt Server Patterns23> **Versions:** Nuxt uses h3 v1 and nitropack v2. Patterns from h3 v2 or nitro v3 docs won't work.45## When to Use67Working with `server/` directory - API routes, server middleware, server utilities.89## Server Directory Structure1011```12server/13├── api/ # API endpoints14│ ├── users.get.ts # GET /api/users15│ ├── users.post.ts # POST /api/users16│ └── users/17│ └── [id].get.ts # GET /api/users/:id18├── routes/ # Non-API routes19│ └── healthz.get.ts # GET /healthz20├── middleware/ # Server middleware21│ └── log.ts22└── utils/ # Server utilities (auto-imported)23└── db.ts24```2526## API Routes2728File naming determines HTTP method and route:2930- `users.get.ts` → GET /api/users31- `users.post.ts` → POST /api/users32- `users/[userId].get.ts` → GET /api/users/:userId33- `users/[userId].delete.ts` → DELETE /api/users/:userId3435**REQUIRED: Use descriptive param names:** `[userId].get.ts` NOT `[id].get.ts`3637## Red Flags - Stop and Check Skill3839If you're thinking any of these, STOP and re-read this skill:4041- "I'll use event.context.params like before"42- "Generic [id] is fine for params"43- "Don't need .get.ts suffix"44- "I remember how Nuxt 3 API routes worked"4546All of these mean: You're using outdated patterns. Use Nuxt 4 patterns instead.4748### Basic API Route4950```ts51// server/api/users.get.ts52export default defineEventHandler(async (event) => {53const users = await fetchUsers()54return users55})56```5758### Route with Params5960```ts61// server/api/users/[userId].get.ts62export default defineEventHandler(async (event) => {63const userId = getRouterParam(event, 'userId')6465if (!userId) {66throw createError({67statusCode: 400,68message: 'User ID is required'69})70}7172const user = await fetchUserById(userId)7374if (!user) {75throw createError({76statusCode: 404,77message: 'User not found'78})79}8081return user82})83```8485### Route with Query Params8687```ts88// server/api/users.get.ts89export default defineEventHandler(async (event) => {90const query = getQuery(event)91const page = Number(query.page) || 192const limit = Number(query.limit) || 109394const users = await fetchUsers({ page, limit })95return users96})97```9899### Route with Body100101```ts102// server/api/users.post.ts103export default defineEventHandler(async (event) => {104const body = await readBody(event)105106// Validate body107if (!body.name || !body.email) {108throw createError({109statusCode: 400,110message: 'Missing required fields: name, email'111})112}113114const user = await createUser(body)115setResponseStatus(event, 201)116return user117})118```119120### Validation with Valibot121122Use `readValidatedBody` and `getValidatedQuery` for schema validation:123124```ts125// server/api/users.post.ts126import * as v from 'valibot'127128const UserSchema = v.object({129name: v.pipe(v.string(), v.minLength(1)),130email: v.pipe(v.string(), v.email())131})132133export default defineEventHandler(async (event) => {134const body = await readValidatedBody(event, v.parser(UserSchema))135// body is typed as { name: string, email: string }136const user = await createUser(body)137setResponseStatus(event, 201)138return user139})140```141142```ts143// server/api/users.get.ts144import * as v from 'valibot'145146const QuerySchema = v.object({147page: v.optional(v.pipe(v.string(), v.transform(Number)), '1'),148limit: v.optional(v.pipe(v.string(), v.transform(Number)), '10')149})150151export default defineEventHandler(async (event) => {152const { page, limit } = await getValidatedQuery(event, v.parser(QuerySchema))153return fetchUsers({ page, limit })154})155```156157## Error Handling158159Use `createError` for HTTP errors:160161```ts162throw createError({163statusCode: 400,164statusMessage: 'Bad Request',165message: 'Invalid input',166data: { field: 'email' } // Optional additional data167})168```169170## Server Middleware171172Runs on every server request:173174```ts175// server/middleware/log.ts176export default defineEventHandler((event) => {177console.log(`${event.method} ${event.path}`)178})179```180181Named middleware for specific patterns:182183```ts184// server/middleware/auth.ts185export default defineEventHandler((event) => {186const token = getRequestHeader(event, 'authorization')187188if (!token) {189throw createError({190statusCode: 401,191message: 'Unauthorized'192})193}194195// Attach user to event context196event.context.user = await verifyToken(token)197})198```199200## Server Utils201202Reusable server functions (auto-imported):203204```ts205// server/utils/db.ts206import { db } from './database'207208export async function fetchUsers(options: { page: number, limit: number }) {209return await db.select().from('users').limit(options.limit).offset((options.page - 1) * options.limit)210}211212export async function fetchUserById(id: string) {213return await db.select().from('users').where({ id }).first()214}215```216217Auto-imported in all server routes and middleware.218219**Import server utils from client (Nuxt 4.3+):**220221```ts222// Use #server alias for type-safe server-only imports223import type { User } from '#server/utils/db'224```225226**Note:** Only types are imported; actual server code never bundles into client.227228## Cached Functions229230Use `defineCachedFunction` for caching expensive operations in server utils:231232```ts233// server/utils/github.ts234export const fetchRepo = defineCachedFunction(235async (owner: string, repo: string) => {236return await $fetch(`https://api.github.com/repos/${owner}/${repo}`)237},238{239maxAge: 60 * 5, // Cache for 5 minutes240swr: true, // Stale-while-revalidate241name: 'github-repo',242getKey: (owner, repo) => `${owner}/${repo}`,243}244)245```246247## Cached Event Handlers248249Use `defineCachedEventHandler` for ISR-style caching on API routes:250251```ts252// server/api/products/[productId].get.ts253export default defineCachedEventHandler(254async (event) => {255const productId = getRouterParam(event, 'productId')256return await fetchProductById(productId)257},258{259maxAge: 3600, // Cache for 1 hour260swr: true, // Serve stale while revalidating261getKey: event => getRouterParam(event, 'productId') ?? '',262}263)264```265266## Generic Error Handler267268Centralize error handling for H3 errors, validation errors, and fallbacks:269270```ts271// server/utils/error-handler.ts272import { isError, createError } from 'h3'273import * as v from 'valibot'274275export function handleApiError(error: unknown, fallback: { statusCode?: number, message: string }): never {276// Re-throw existing H3 errors277if (isError(error)) throw error278279// Handle Valibot validation errors280if (v.isValiError(error)) {281throw createError({ statusCode: 400, message: error.issues[0].message })282}283284// Generic fallback285throw createError({ statusCode: fallback.statusCode ?? 502, message: fallback.message })286}287```288289Usage in routes:290291```ts292export default defineEventHandler(async (event) => {293try {294const data = await fetchExternalApi()295return data296} catch (error) {297handleApiError(error, { statusCode: 502, message: 'Failed to fetch data' })298}299})300```301302## Request Helpers303304```ts305// Get params306const userId = getRouterParam(event, 'userId')307308// Get query309const query = getQuery(event)310311// Get body312const body = await readBody(event)313314// Get headers315const auth = getRequestHeader(event, 'authorization')316317// Get cookies318const token = getCookie(event, 'token')319320// Get method321const method = getMethod(event)322323// Get IP324const ip = getRequestIP(event)325```326327## Response Helpers328329```ts330// Set status code331setResponseStatus(event, 201)332333// Set headers334setResponseHeader(event, 'X-Custom', 'value')335setResponseHeaders(event, { 'X-Custom': 'value', 'X-Another': 'value' })336337// Set cookies338setCookie(event, 'token', 'value', {339httpOnly: true,340secure: true,341sameSite: 'lax',342maxAge: 60 * 60 * 24 * 7 // 1 week343})344345// Redirect346return sendRedirect(event, '/login', 302)347348// Stream349return sendStream(event, stream)350351// No content352return sendNoContent(event)353```354355## Background Tasks356357Use `event.waitUntil()` for async tasks that shouldn't block the response (Nuxt 4+):358359```ts360// server/api/analytics.post.ts361export default defineEventHandler(async (event) => {362const data = await readBody(event)363364// Don't block response with analytics logging365event.waitUntil(366logAnalytics(data)367)368369return { success: true }370})371```372373**Use cases:** logging, caching, background processing, async cleanup.374375## Best Practices376377- **Use descriptive param names** - `[userId]` not `[id]`378- **Keep routes thin** - delegate to server utils379- **Validate input** at route level380- **Use typed errors** with createError381- **Handle errors gracefully** - don't expose internals382- **Use server utils** for DB/external APIs383- **Don't expose sensitive data** in responses384- **Set proper status codes** - 201 for created, 204 for no content385- **Use event.waitUntil()** for background tasks that shouldn't block responses386387## Common Mistakes388389| ❌ Wrong | ✅ Right |390| ------------------------- | ----------------------------- |391| `event.context.params.id` | `getRouterParam(event, 'id')` |392| `return res.json(data)` | `return data` |393| `[id].get.ts` | `[userId].get.ts` |394| `users-id.get.ts` | `users/[id].get.ts` |395| Throw generic errors | Use createError with status |396397## WebSocket398399```ts400// server/routes/_ws.ts401export default defineWebSocketHandler({402open(peer) {403console.log('Client connected:', peer.id)404},405message(peer, message) {406peer.send(`Echo: ${message.text()}`)407// Broadcast to all: peer.publish('channel', message)408},409close(peer) {410console.log('Client disconnected:', peer.id)411}412})413```414415Enable in config:416417```ts418// nuxt.config.ts419export default defineNuxtConfig({420nitro: {421experimental: { websocket: true }422}423})424```425426## Server-Sent Events (Experimental)427428```ts429// server/api/stream.get.ts430export default defineEventHandler(async (event) => {431const stream = createEventStream(event)432433const interval = setInterval(async () => {434await stream.push({ data: JSON.stringify({ time: Date.now() }) })435}, 1000)436437stream.onClosed(() => {438clearInterval(interval)439})440441return stream.send()442})443```444445## Resources446447- Nuxt server: https://nuxt.com/docs/guide/directory-structure/server448- h3 (Nitro engine): https://v1.h3.dev/449- Nitro: https://nitro.build/450451> **For database/storage APIs:** see `nuxthub` skill452