Nuxt 3 advanced patterns unlock full-stack capabilities — defineEventHandler(async (event) => { ... }) creates server routes in server/api/. readBody(event) and getQuery(event) parse request data. useFetch("/api/posts") fetches from server routes with auto-typing. useAsyncData("key", () => $fetch("/api/posts")) fetches with manual cache key management. useState<T>("key", () => initialValue) creates SSR-safe reactive global state. useNuxtApp() accesses the Nuxt instance, plugins, and $fetch. Layers: extends: ["./base-layer"] in nuxt.config.ts inherits config, components, composables, and plugins. Server middleware: server/middleware/auth.ts runs on every request. Nitro plugins: server/plugins/startup.ts runs once at server start. Route rules: routeRules: { "/blog/**": { isr: 3600 }, "/api/**": { cors: true } }. $fetch is Nitro’s type-aware fetch. useRequestHeaders(["cookie"]) passes cookies server-to-server. Claude Code generates Nuxt server routes, typed composables, layer architecture, and nitro plugins.
CLAUDE.md for Nuxt Advanced
## Nuxt Advanced Stack
- Version: nuxt >= 3.13, @nuxtjs/tailwindcss >= 6.12
- Server routes: server/api/posts/index.get.ts — defineEventHandler with .get/.post/.put/.delete suffix
- Body: const body = await readBody<CreatePostBody>(event)
- Query: const query = getQuery(event) — parsed from URL
- Auth: const session = await getUserSession(event) — with @sidebase/nuxt-auth or nuxt-auth-utils
- useFetch: auto-imported, returns { data, pending, error, execute, refresh }
- useState: const count = useState<number>("counter", () => 0) — SSR-safe global
- Middleware: defineNuxtRouteMiddleware((to) => { if (!auth.loggedIn) return navigateTo("/login") })
Server API Routes
// server/api/posts/index.get.ts — GET /api/posts
import { defineEventHandler, getQuery } from "h3"
import { db } from "~/server/db"
import { posts, users } from "~/server/db/schema"
import { eq, ilike, desc, count, and } from "drizzle-orm"
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const page = Math.max(1, parseInt(String(query.page ?? "1"), 10))
const pageSize = Math.min(50, Math.max(1, parseInt(String(query.pageSize ?? "10"), 10)))
const search = String(query.search ?? "")
const offset = (page - 1) * pageSize
const where = []
if (search) where.push(ilike(posts.title, `%${search}%`))
where.push(eq(posts.published, true))
const [result, totalResult] = await Promise.all([
db.query.posts.findMany({
where: and(...where),
limit: pageSize,
offset,
orderBy: desc(posts.createdAt),
with: { author: { columns: { id: true, name: true, avatarUrl: true } } },
}),
db.select({ count: count() }).from(posts).where(and(...where)),
])
return {
posts: result,
total: totalResult[0]?.count ?? 0,
page,
pageSize,
totalPages: Math.ceil((totalResult[0]?.count ?? 0) / pageSize),
}
})
// server/api/posts/index.post.ts — POST /api/posts (authenticated)
import { defineEventHandler, readBody, createError } from "h3"
import { z } from "zod"
import { getUserSession } from "#auth"
import { db } from "~/server/db"
import { posts } from "~/server/db/schema"
const CreatePostSchema = z.object({
title: z.string().min(5).max(200),
content: z.string().min(20),
excerpt: z.string().max(500).optional(),
published: z.boolean().default(false),
tags: z.array(z.string()).default([]),
})
export default defineEventHandler(async (event) => {
const session = await getUserSession(event)
if (!session?.user) {
throw createError({ statusCode: 401, statusMessage: "Unauthorized" })
}
const rawBody = await readBody(event)
const parsed = CreatePostSchema.safeParse(rawBody)
if (!parsed.success) {
throw createError({
statusCode: 400,
statusMessage: "Validation error",
data: parsed.error.flatten().fieldErrors,
})
}
const { title, content, excerpt, published, tags } = parsed.data
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")
const [post] = await db.insert(posts).values({
title,
slug,
content,
excerpt,
published,
authorId: session.user.id,
publishedAt: published ? new Date() : null,
}).returning()
setResponseStatus(event, 201)
return post
})
// server/api/posts/[id].delete.ts — DELETE /api/posts/:id
import { defineEventHandler, createError, getRouterParam } from "h3"
import { getUserSession } from "#auth"
import { db } from "~/server/db"
import { posts } from "~/server/db/schema"
import { eq, and } from "drizzle-orm"
export default defineEventHandler(async (event) => {
const session = await getUserSession(event)
if (!session?.user) {
throw createError({ statusCode: 401, statusMessage: "Unauthorized" })
}
const id = getRouterParam(event, "id")!
const existing = await db.query.posts.findFirst({ where: eq(posts.id, id) })
if (!existing) throw createError({ statusCode: 404, statusMessage: "Post not found" })
if (existing.authorId !== session.user.id && session.user.role !== "admin") {
throw createError({ statusCode: 403, statusMessage: "Forbidden" })
}
await db.delete(posts).where(eq(posts.id, id))
setResponseStatus(event, 204)
return null
})
Composables with Auto-Import
// composables/usePosts.ts — reactive post fetching composable
export function usePosts(options: {
page?: Ref<number>
pageSize?: number
search?: Ref<string>
} = {}) {
const page = options.page ?? ref(1)
const pageSize = options.pageSize ?? 10
const search = options.search ?? ref("")
const { data, pending, error, refresh } = useFetch("/api/posts", {
query: computed(() => ({
page: page.value,
pageSize,
search: search.value || undefined,
})),
watch: [page, search],
})
return {
posts: computed(() => data.value?.posts ?? []),
total: computed(() => data.value?.total ?? 0),
totalPages: computed(() => data.value?.totalPages ?? 0),
pending,
error,
refresh,
page,
}
}
// composables/usePost.ts — single post with refresh
export function usePost(id: MaybeRef<string>) {
return useFetch(() => `/api/posts/${unref(id)}`, {
watch: [toRef(id)],
lazy: false,
})
}
// composables/useAuth.ts — authentication composable
export function useAuth() {
const { loggedIn, user, session, fetch: refreshSession, clear } = useUserSession()
const isAdmin = computed(() => user.value?.role === "admin")
async function login(email: string, password: string) {
const { data, error } = await useFetch("/api/auth/login", {
method: "POST",
body: { email, password },
})
if (error.value) throw error.value
await refreshSession()
return data.value
}
async function logout() {
await $fetch("/api/auth/logout", { method: "POST" })
await clear()
return navigateTo("/")
}
return {
loggedIn,
user,
session,
isAdmin,
login,
logout,
}
}
Route Middleware
// middleware/auth.ts — route-level auth guard
export default defineNuxtRouteMiddleware((to) => {
const { loggedIn } = useUserSession()
if (!loggedIn.value) {
return navigateTo(`/login?redirect=${encodeURIComponent(to.fullPath)}`)
}
})
// middleware/admin.ts — admin-only guard
export default defineNuxtRouteMiddleware(() => {
const { loggedIn, user } = useUserSession()
if (!loggedIn.value) return navigateTo("/login")
if (user.value?.role !== "admin") return abortNavigation({ statusCode: 403 })
})
Nuxt Config with Route Rules
// nuxt.config.ts — advanced Nuxt configuration
export default defineNuxtConfig({
modules: [
"@nuxtjs/tailwindcss",
"nuxt-auth-utils",
"@nuxt/image",
"@nuxtjs/fontaine",
"nuxt-og-image",
],
runtimeConfig: {
// Server-only secrets
databaseUrl: process.env.DATABASE_URL,
sessionSecret: process.env.SESSION_SECRET,
jwtSecret: process.env.JWT_SECRET,
// Public — accessible from the frontend
public: {
apiUrl: process.env.NUXT_PUBLIC_API_URL ?? "/api",
appName: "My App",
},
},
routeRules: {
// Homepage — SSG at build time
"/": { prerender: true },
// Blog listing — ISR: regenerate at most every hour
"/blog": { isr: 3600 },
"/blog/**": { isr: 3600 },
// Dashboard — no SSR, client-only
"/dashboard/**": { ssr: false },
// API — CORS headers
"/api/**": {
cors: true,
headers: { "cache-control": "no-cache" },
},
},
nitro: {
experimental: {
database: true,
},
preset: "cloudflare-pages",
},
typescript: {
strict: true,
typeCheck: true,
},
experimental: {
payloadExtraction: true,
viewTransition: true,
},
})
For the SvelteKit alternative when a leaner, non-virtual-DOM framework that compiles away reactivity overhead, has simpler file-based routing with load functions, and produces smaller client bundles is preferred — SvelteKit is faster at runtime while Nuxt has a larger ecosystem and Vue’s Options/Composition API, see the SvelteKit advanced guide. For the Next.js alternative when React is the preferred component model, the App Router’s Server Components reduce client JavaScript, and the larger React ecosystem (libraries, hiring) is a priority — Next.js and Nuxt are architecturally similar but target different frontend frameworks, see the Next.js App Router guide. The Claude Skills 360 bundle includes Nuxt advanced skill sets covering server routes, composables, and layers. Start with the free tier to try full-stack Nuxt generation.