Claude Code Plugins

Community-maintained marketplace

Feedback

Build type-safe Nuxt 3 applications with Nitro API patterns. Covers validation, fetch patterns, auth, SSR, composables, background tasks, and real-time features.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name nuxt-nitro-api
description Build type-safe Nuxt 3 applications with Nitro API patterns. Covers validation, fetch patterns, auth, SSR, composables, background tasks, and real-time features.

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:

Example Files

Working examples from a Nuxt project:

Core Principles

  1. Let Nitro infer types - Never add manual type params to $fetch<Type>() or useFetch<Type>()
  2. Use h3 validation - getValidatedQuery(), readValidatedBody() with Zod schemas
  3. Composables for context, utils for pure functions - Composables access Nuxt context, utils are pure
  4. SSR-safe code - Guard browser APIs with import.meta.client or onMounted
  5. 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

  1. Don't use $fetch at top level - Causes double-fetch (SSR + client). Use useFetch.
  2. Debounce search inputs - Use refDebounced to avoid excessive API calls.
  3. Reset pagination on filter change - Or users see empty page 5 with new filters.
  4. Guard browser APIs - Use import.meta.client, onMounted, or <ClientOnly>.
  5. Nitro tasks are single-instance - Can't run same task twice concurrently. Use DB job queue.
  6. useRouteQuery needs Nuxt composables - Pass route and router explicitly.
  7. Input types aren't auto-generated - Export Zod schemas for client use.
  8. Cookie size limit is 4096 bytes - Store only essential session data.