src/skills/web-meta-framework-nuxt/SKILL.md
Nuxt patterns - file-based routing, data fetching (useFetch/useAsyncData), useState, server routes, middleware, auto-imports, layouts, SEO
npx skillsauth add agents-inc/skills web-meta-framework-nuxtInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use
useFetchfor API calls in components (SSR-safe),useAsyncDatafor custom data sources or parallel fetches. Create server routes inserver/api/. Auto-imports handle composables and components automatically. UseuseStatefor SSR-friendly shared state. Data is ashallowRefby default -- usedeep: trueif you need deep reactivity.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use useFetch or useAsyncData for data fetching in components -- NEVER raw $fetch in setup which causes double-fetching)
(You MUST use server/api/ for API routes -- handlers export default with defineEventHandler())
(You MUST use definePageMeta to attach middleware and configure page behavior -- it is a macro, values must be statically analyzable)
(You MUST use useHead or useSeoMeta for SEO metadata -- never manual <head> tags)
(You MUST ensure useState values are JSON-serializable for SSR hydration -- no functions, classes, or Symbols)
</critical_requirements>
Auto-detection: Nuxt, nuxt.config.ts, useFetch, useAsyncData, useState, defineEventHandler, definePageMeta, defineNuxtRouteMiddleware, NuxtLayout, NuxtPage, NuxtLink, navigateTo, server/api, pages/, layouts/, middleware/, composables/, useHead, useSeoMeta, app/ directory
When to use:
Key patterns covered:
When NOT to use:
Nuxt is a meta-framework for Vue 3 that provides file-based routing, automatic code splitting, server-side rendering, and a powerful data-fetching system. Built on Nitro server engine, it enables full-stack development with API routes colocated with your frontend.
Core Principles:
data from useFetch/useAsyncData is a shallowRef by defaultFile names in pages/ become URL paths. Dynamic segments use bracket syntax.
| File | URL | Description |
| --------------------------- | ------------------------ | ------------------ |
| pages/index.vue | / | Home page |
| pages/about.vue | /about | Static route |
| pages/blog/[slug].vue | /blog/:slug | Dynamic parameter |
| pages/users/[...slug].vue | /users/* | Catch-all route |
| pages/posts/[[id]].vue | /posts or /posts/:id | Optional parameter |
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute();
const slug = route.params.slug as string;
const { data: post, error } = await useFetch(`/api/posts/${slug}`);
if (error.value) {
throw createError({ statusCode: 404, statusMessage: "Post not found" });
}
</script>
Why good: File names map to URLs, bracket syntax for dynamic params, createError triggers error page
See examples/core.md for complete page examples with layouts and middleware.
useFetch wraps useAsyncData + $fetch. It prevents double-fetching by transferring server data to client during hydration. Data is a shallowRef -- replace the whole object to trigger reactivity, or use deep: true.
// Simple fetch -- URL is cache key
const { data, error, status, refresh, clear } = await useFetch("/api/users");
// With reactive query params and auto-refetch
const page = ref(1);
const { data: users } = await useFetch("/api/users", {
query: { page, limit: 20 },
watch: [page],
});
// POST with immediate: false for user-triggered actions
const { execute, status } = useFetch("/api/users", {
method: "POST",
body: form,
immediate: false,
watch: false,
});
Use useAsyncData when combining multiple fetches or using non-HTTP sources:
const { data } = await useAsyncData("dashboard", async () => {
const [users, stats] = await Promise.all([
$fetch("/api/users"),
$fetch("/api/stats"),
]);
return { users, stats };
});
Critical: $fetch in <script setup> (outside useFetch/useAsyncData) runs on both server and client, causing double-fetching. Always wrap in a composable.
See examples/data-fetching.md for typed responses, transforms, lazy loading, and server-only fetch patterns.
Server routes live in server/api/ (prefixed with /api) or server/routes/ (no prefix). File suffix restricts HTTP method.
// server/api/users.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const page = Number(query.page) || 1;
return db.users.findMany({ skip: (page - 1) * 20, take: 20 });
});
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event);
// Validate body with Zod or similar
setResponseStatus(event, 201);
return db.users.create({ data: body });
});
| Pattern | File | URL |
| --------- | -------------------------- | ----------------- |
| GET | server/api/users.get.ts | GET /api/users |
| POST | server/api/users.post.ts | POST /api/users |
| Dynamic | server/api/users/[id].ts | /api/users/:id |
| Catch-all | server/api/[...path].ts | /api/* |
| No prefix | server/routes/health.ts | /health |
See examples/server-routes.md for validation, error handling, server middleware, and CRUD patterns.
useState is an SSR-friendly composable for shared reactive state. Values transfer from server to client during hydration and must be JSON-serializable.
// composables/use-user.ts
export function useUser() {
const user = useState<User | null>("user", () => null);
const isLoggedIn = computed(() => user.value !== null);
async function login(credentials: { email: string; password: string }) {
user.value = await $fetch<User>("/api/auth/login", {
method: "POST",
body: credentials,
});
}
return { user: readonly(user), isLoggedIn, login };
}
Key constraints: Values must be JSON-serializable (no functions, classes). Key ensures singleton sharing across components. Wrap mutations in composable functions.
See examples/state-management.md for cart state, UI state, cookie persistence, and server-initialized patterns.
Middleware runs before navigation. Use for auth, authorization, and redirects.
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isLoggedIn } = useUser();
if (!isLoggedIn.value) {
return navigateTo(`/login?redirect=${encodeURIComponent(to.fullPath)}`);
}
});
| Type | File Pattern | Behavior |
| ------ | --------------------------- | ------------------------- |
| Named | middleware/auth.ts | Opt-in via definePageMeta |
| Global | middleware/auth.global.ts | Runs on every navigation |
| Inline | Function in definePageMeta | Page-specific logic |
Attach via definePageMeta({ middleware: "auth" }) or definePageMeta({ middleware: ["auth", "admin"] }).
Critical: Use to and from parameters -- never useRoute() in middleware (may have stale values).
See examples/middleware.md for role-based auth, feature flags, guest guards, and global middleware patterns.
Layouts wrap pages with shared UI (navigation, footers). Default layout applies automatically.
<!-- layouts/default.vue -->
<template>
<div class="layout">
<header>
<nav><!-- Navigation --></nav>
</header>
<main><slot /></main>
<footer><!-- Footer --></footer>
</div>
</template>
Select layout per page: definePageMeta({ layout: "admin" }). Dynamic layout: <NuxtLayout :name="computedLayout">.
See examples/core.md for layout examples with auth-aware navigation.
<script setup lang="ts">
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`);
useSeoMeta({
title: () => post.value?.title ?? "Blog Post",
description: () => post.value?.excerpt ?? "",
ogTitle: () => post.value?.title ?? "Blog Post",
ogImage: () => post.value?.coverImage ?? "/default-og.png",
twitterCard: "summary_large_image",
});
</script>
Why good: Reactive values with getter functions, type-safe property names, automatic Open Graph and Twitter cards, SSR-rendered
Global defaults in nuxt.config.ts via app.head. Page-level overrides via composables.
Plugins run before Vue app creation. Use for registering global utilities or external libraries.
// plugins/api.client.ts -- .client suffix = browser only
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
const api = $fetch.create({
baseURL: config.public.apiBase,
onRequest({ options }) {
const token = useCookie("token");
if (token.value) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token.value}`,
};
}
},
});
return { provide: { api } };
});
Access via useNuxtApp().$api. Suffixes: .client.ts (browser), .server.ts (server), no suffix (both).
// Server route errors
throw createError({
statusCode: 404,
statusMessage: "Not found",
data: { id },
});
// Page-level: check useFetch error, throw createError
// Component-level: NuxtErrorBoundary with #error slot
// Global: error.vue at root level with clearError({ redirect: "/" })
createError works in both server and client. NuxtErrorBoundary isolates component failures. Root error.vue catches unhandled errors.
See examples/core.md for error page and boundary examples.
</patterns>Detailed Resources:
<red_flags>
High Priority Issues:
$fetch directly in <script setup> for initial data -- causes double-fetching (server + client)useState -- functions, classes, Symbols cause hydration errorskey in useAsyncData for dynamic data -- leads to stale data and caching issuesuseRoute() in middleware -- use to and from parameters instead; useRoute may have stale valuesruntimeConfig private keys for server-only secretsMedium Priority Issues:
lazy: true -- slows navigation; use lazy for non-critical dataerror.valueonMounted for data that should be in useFetch -- misses SSR benefitsawait before useFetch in setup -- component renders before data is readyGotchas & Edge Cases:
useFetch URL is the cache key -- same URL = same cached data; use key option to differentiateuseState runs initializer only once per key -- subsequent calls return existing stateimport.meta.server/import.meta.client to splitserver/api/ routes auto-prefix with /api -- server/api/users.ts becomes /api/usersdefinePageMeta is a macro, not runtime -- values must be statically analyzableNuxtLink with external URLs needs external prop or use <a> insteadawait before first composable callwatch in useFetch requires reactive values -- plain variables won't trigger refetchdata from useFetch/useAsyncData is a shallowRef -- mutating nested properties won't trigger reactivity; replace the whole object or use deep: truedata and error default to undefined (not null) -- adjust null checks accordingly</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST use useFetch or useAsyncData for data fetching in components -- NEVER raw $fetch in setup which causes double-fetching)
(You MUST use server/api/ for API routes -- handlers export default with defineEventHandler())
(You MUST use definePageMeta to attach middleware and configure page behavior -- it is a macro, values must be statically analyzable)
(You MUST use useHead or useSeoMeta for SEO metadata -- never manual <head> tags)
(You MUST ensure useState values are JSON-serializable for SSR hydration -- no functions, classes, or Symbols)
Failure to follow these rules will cause SSR hydration mismatches, double-fetching, and broken page metadata.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety