Nuxt 3 / Nitro API Patterns
This skill provides patterns for building type-safe Nuxt 3 applications with Nitro backends.
When to Use This Skill
Use this skill when:
- Working in a Nuxt 3 project with TypeScript
- Building API endpoints with Nitro
- Implementing authentication with nuxt-auth-utils
- Handling SSR + client-side state
- Creating background tasks or real-time features
Reference Files
For detailed patterns, see these topic-focused reference files:
- validation.md - Zod validation with h3, Standard Schema, error handling
- fetch-patterns.md - useFetch vs $fetch vs useAsyncData
- auth-patterns.md - nuxt-auth-utils, OAuth, WebAuthn, middleware
- page-structure.md - Keep pages thin, components do the work
- composables-utils.md - When to use composables vs utils
- ssr-client.md - SSR + localStorage, hydration, VueUse
- deep-linking.md - URL params sync with filters and useFetch
- nitro-tasks.md - Background jobs, scheduled tasks, job queues
- sse.md - Server-Sent Events for real-time streaming
- server-services.md - Third-party service integration patterns
Example Files
Working examples from a Nuxt project:
Core Principles
- Let Nitro infer types - Never add manual type params to
$fetch<Type>() or useFetch<Type>()
- Use h3 validation -
getValidatedQuery(), readValidatedBody() with Zod schemas
- Composables for context, utils for pure functions - Composables access Nuxt context, utils are pure
- SSR-safe code - Guard browser APIs with
import.meta.client or onMounted
- Keep pages thin - Pages = layout + route params + components. Components own data fetching and logic.
Auto-Imports Quick Reference
Server-side (/server directory)
All h3 utilities auto-imported:
defineEventHandler, createError, getQuery, getValidatedQuery
readBody, readValidatedBody, getRouterParams, getValidatedRouterParams
getCookie, setCookie, deleteCookie, getHeader, setHeader
From nuxt-auth-utils:
getUserSession, setUserSession, clearUserSession, requireUserSession
hashPassword, verifyPassword
defineOAuth*EventHandler (Google, GitHub, etc.)
Need to import: z from "zod", fromZodError from "zod-validation-error"
Client-side
All auto-imported:
- Vue:
ref, computed, watch, onMounted, etc.
- VueUse:
refDebounced, useLocalStorage, useUrlSearchParams, etc.
- Nuxt:
useFetch, useAsyncData, useRoute, useRouter, useState, navigateTo
Shared (/shared directory - Nuxt 3.14+)
Code auto-imported on both client AND server. Use for:
- Types and interfaces
- Pure utility functions
- Constants
Quick Patterns
Validation (h3 v2+ with Standard Schema)
// Pass Zod schema directly (h3 v2+)
const query = await getValidatedQuery(event, z.object({
search: z.string().optional(),
page: z.coerce.number().default(1),
}));
const body = await readValidatedBody(event, z.object({
email: z.string().email(),
name: z.string().min(1),
}));
$fetch Type Inference
// Template literals preserve type inference (fixed late 2024)
const userId = "123"; // Literal type "123"
const result = await $fetch(`/api/users/${userId}`);
// result is typed from the handler's return type
// NEVER do this - defeats type inference
const result = await $fetch<User>("/api/users/123"); // WRONG
useFetch for Page Data
// Basic - types inferred from Nitro
const { data, status, refresh } = await useFetch("/api/users");
// Reactive query params - auto-refetch on change
const search = ref("");
const debouncedSearch = refDebounced(search, 300); // Auto-imported
const { data } = await useFetch("/api/users", {
query: computed(() => ({
...(debouncedSearch.value ? { search: debouncedSearch.value } : {}),
})),
});
// Dynamic URL with getter
const userId = ref("123");
const { data } = await useFetch(() => `/api/users/${userId.value}`);
// New options (Nuxt 3.14+)
const { data } = await useFetch("/api/data", {
retry: 3, // Retry on failure
retryDelay: 1000, // Wait between retries
dedupe: "cancel", // Cancel previous request
delay: 300, // Debounce the request
});
$fetch for Event Handlers
// ONLY use $fetch in event handlers (onClick, onSubmit)
const handleSubmit = async () => {
const result = await $fetch("/api/users", {
method: "POST",
body: { name: "Test" },
});
};
Auth Check in API
// In server/utils/auth.ts
export async function getAuthenticatedUser(event: H3Event) {
const session = await getUserSession(event);
if (!session?.user) {
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
}
return session.user;
}
// In API handler
export default defineEventHandler(async (event) => {
const user = await getAuthenticatedUser(event);
// user is typed and guaranteed to exist
});
SSR-Safe localStorage
// Option 1: import.meta.client guard
watch(preference, (value) => {
if (import.meta.client) {
localStorage.setItem("pref", value);
}
});
// Option 2: onMounted
onMounted(() => {
const saved = localStorage.getItem("pref");
if (saved) preference.value = saved;
});
// Option 3: VueUse (SSR-safe)
const theme = useLocalStorage("theme", "light");
Composable vs Util Decision
Needs Nuxt/Vue context (useRuntimeConfig, useRoute, refs)?
├─ YES → COMPOSABLE in /composables/use*.ts
└─ NO → UTIL in /utils/*.ts (client) or /server/utils/*.ts (server)
Key Gotchas
- Don't use
$fetch at top level - Causes double-fetch (SSR + client). Use useFetch.
- Debounce search inputs - Use
refDebounced to avoid excessive API calls.
- Reset pagination on filter change - Or users see empty page 5 with new filters.
- Guard browser APIs - Use
import.meta.client, onMounted, or <ClientOnly>.
- Nitro tasks are single-instance - Can't run same task twice concurrently. Use DB job queue.
- useRouteQuery needs Nuxt composables - Pass
route and router explicitly.
- Input types aren't auto-generated - Export Zod schemas for client use.
- Cookie size limit is 4096 bytes - Store only essential session data.
1---2name: nuxt-nitro-api-23description: Build type-safe Nuxt 3 applications with Nitro API patterns. Covers validation, fetch patterns, auth, SSR, composables, background tasks, and real-time features.4---56# Nuxt 3 / Nitro API Patterns78This skill provides patterns for building type-safe Nuxt 3 applications with Nitro backends.910## When to Use This Skill1112Use this skill when:13- Working in a Nuxt 3 project with TypeScript14- Building API endpoints with Nitro15- Implementing authentication with nuxt-auth-utils16- Handling SSR + client-side state17- Creating background tasks or real-time features1819## Reference Files2021For detailed patterns, see these topic-focused reference files:2223- [validation.md](./validation.md) - Zod validation with h3, Standard Schema, error handling24- [fetch-patterns.md](./fetch-patterns.md) - useFetch vs $fetch vs useAsyncData25- [auth-patterns.md](./auth-patterns.md) - nuxt-auth-utils, OAuth, WebAuthn, middleware26- [page-structure.md](./page-structure.md) - Keep pages thin, components do the work27- [composables-utils.md](./composables-utils.md) - When to use composables vs utils28- [ssr-client.md](./ssr-client.md) - SSR + localStorage, hydration, VueUse29- [deep-linking.md](./deep-linking.md) - URL params sync with filters and useFetch30- [nitro-tasks.md](./nitro-tasks.md) - Background jobs, scheduled tasks, job queues31- [sse.md](./sse.md) - Server-Sent Events for real-time streaming32- [server-services.md](./server-services.md) - Third-party service integration patterns3334## Example Files3536Working examples from a Nuxt project:3738- [validation-endpoint.ts](./examples/validation-endpoint.ts) - API endpoint with Zod validation39- [auth-middleware.ts](./examples/auth-middleware.ts) - Server auth middleware40- [auth-utils.ts](./examples/auth-utils.ts) - Reusable auth helpers41- [deep-link-page.vue](./examples/deep-link-page.vue) - URL params sync with filters42- [sse-endpoint.ts](./examples/sse-endpoint.ts) - SSE streaming endpoint43- [service-util.ts](./examples/service-util.ts) - Server-side service pattern4445## Core Principles46471. **Let Nitro infer types** - Never add manual type params to `$fetch<Type>()` or `useFetch<Type>()`482. **Use h3 validation** - `getValidatedQuery()`, `readValidatedBody()` with Zod schemas493. **Composables for context, utils for pure functions** - Composables access Nuxt context, utils are pure504. **SSR-safe code** - Guard browser APIs with `import.meta.client` or `onMounted`515. **Keep pages thin** - Pages = layout + route params + components. Components own data fetching and logic.5253## Auto-Imports Quick Reference5455### Server-side (`/server` directory)5657All h3 utilities auto-imported:58- `defineEventHandler`, `createError`, `getQuery`, `getValidatedQuery`59- `readBody`, `readValidatedBody`, `getRouterParams`, `getValidatedRouterParams`60- `getCookie`, `setCookie`, `deleteCookie`, `getHeader`, `setHeader`6162From nuxt-auth-utils:63- `getUserSession`, `setUserSession`, `clearUserSession`, `requireUserSession`64- `hashPassword`, `verifyPassword`65- `defineOAuth*EventHandler` (Google, GitHub, etc.)6667**Need to import:** `z` from "zod", `fromZodError` from "zod-validation-error"6869### Client-side7071All auto-imported:72- Vue: `ref`, `computed`, `watch`, `onMounted`, etc.73- VueUse: `refDebounced`, `useLocalStorage`, `useUrlSearchParams`, etc.74- Nuxt: `useFetch`, `useAsyncData`, `useRoute`, `useRouter`, `useState`, `navigateTo`7576### Shared (`/shared` directory - Nuxt 3.14+)7778Code auto-imported on both client AND server. Use for:79- Types and interfaces80- Pure utility functions81- Constants8283## Quick Patterns8485### Validation (h3 v2+ with Standard Schema)8687```typescript88// Pass Zod schema directly (h3 v2+)89const query = await getValidatedQuery(event, z.object({90 search: z.string().optional(),91 page: z.coerce.number().default(1),92}));9394const body = await readValidatedBody(event, z.object({95 email: z.string().email(),96 name: z.string().min(1),97}));98```99100### $fetch Type Inference101102```typescript103// Template literals preserve type inference (fixed late 2024)104const userId = "123"; // Literal type "123"105const result = await $fetch(`/api/users/${userId}`);106// result is typed from the handler's return type107108// NEVER do this - defeats type inference109const result = await $fetch<User>("/api/users/123"); // WRONG110```111112### useFetch for Page Data113114```typescript115// Basic - types inferred from Nitro116const { data, status, refresh } = await useFetch("/api/users");117118// Reactive query params - auto-refetch on change119const search = ref("");120const debouncedSearch = refDebounced(search, 300); // Auto-imported121const { data } = await useFetch("/api/users", {122 query: computed(() => ({123 ...(debouncedSearch.value ? { search: debouncedSearch.value } : {}),124 })),125});126127// Dynamic URL with getter128const userId = ref("123");129const { data } = await useFetch(() => `/api/users/${userId.value}`);130131// New options (Nuxt 3.14+)132const { data } = await useFetch("/api/data", {133 retry: 3, // Retry on failure134 retryDelay: 1000, // Wait between retries135 dedupe: "cancel", // Cancel previous request136 delay: 300, // Debounce the request137});138```139140### $fetch for Event Handlers141142```typescript143// ONLY use $fetch in event handlers (onClick, onSubmit)144const handleSubmit = async () => {145 const result = await $fetch("/api/users", {146 method: "POST",147 body: { name: "Test" },148 });149};150```151152### Auth Check in API153154```typescript155// In server/utils/auth.ts156export async function getAuthenticatedUser(event: H3Event) {157 const session = await getUserSession(event);158 if (!session?.user) {159 throw createError({ statusCode: 401, statusMessage: "Unauthorized" });160 }161 return session.user;162}163164// In API handler165export default defineEventHandler(async (event) => {166 const user = await getAuthenticatedUser(event);167 // user is typed and guaranteed to exist168});169```170171### SSR-Safe localStorage172173```typescript174// Option 1: import.meta.client guard175watch(preference, (value) => {176 if (import.meta.client) {177 localStorage.setItem("pref", value);178 }179});180181// Option 2: onMounted182onMounted(() => {183 const saved = localStorage.getItem("pref");184 if (saved) preference.value = saved;185});186187// Option 3: VueUse (SSR-safe)188const theme = useLocalStorage("theme", "light");189```190191### Composable vs Util Decision192193```194Needs Nuxt/Vue context (useRuntimeConfig, useRoute, refs)?195├─ YES → COMPOSABLE in /composables/use*.ts196└─ NO → UTIL in /utils/*.ts (client) or /server/utils/*.ts (server)197```198199## Key Gotchas2002011. **Don't use `$fetch` at top level** - Causes double-fetch (SSR + client). Use `useFetch`.2022. **Debounce search inputs** - Use `refDebounced` to avoid excessive API calls.2033. **Reset pagination on filter change** - Or users see empty page 5 with new filters.2044. **Guard browser APIs** - Use `import.meta.client`, `onMounted`, or `<ClientOnly>`.2055. **Nitro tasks are single-instance** - Can't run same task twice concurrently. Use DB job queue.2066. **useRouteQuery needs Nuxt composables** - Pass `route` and `router` explicitly.2077. **Input types aren't auto-generated** - Export Zod schemas for client use.2088. **Cookie size limit is 4096 bytes** - Store only essential session data.