Claude Code for Nuxt Advanced: Server Routes, Composables, and Layers — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Nuxt Advanced: Server Routes, Composables, and Layers
Frontend

Claude Code for Nuxt Advanced: Server Routes, Composables, and Layers

Published: June 11, 2027
Read time: 6 min read
By: Claude Skills 360

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.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free