●WWDC — WWDC 2026 confirms Siri runs on Google Gemini; third-party handoff to ChatGPT is dropped, and Siri AI won't ship in the EU under the DMA at iOS 27●BILLING — 6 days until the Jun 15 change: Agent SDK, headless Claude Code, GitHub Actions, and third-party agents move to API-rate monthly credit●OUTAGE — claude.ai, Claude Code, and Cowork saw an outage (Jun). Scheduled runs are safest when built around fallbackModel and retries●DYNAMIC-WORKFLOWS — Dynamic workflows are on by default on Max/Team and the API, for codebase-wide bug hunts and independent verification●ULTRACODE — Claude Code's new ultracode setting sits in the effort menu, fixing effort to xhigh while Claude decides when to run a workflow●OPUS4.8 — Claude Opus 4.8 is settled in as the default across major plans, with stronger coding, agentic, and reasoning skills●WWDC — WWDC 2026 confirms Siri runs on Google Gemini; third-party handoff to ChatGPT is dropped, and Siri AI won't ship in the EU under the DMA at iOS 27●BILLING — 6 days until the Jun 15 change: Agent SDK, headless Claude Code, GitHub Actions, and third-party agents move to API-rate monthly credit●OUTAGE — claude.ai, Claude Code, and Cowork saw an outage (Jun). Scheduled runs are safest when built around fallbackModel and retries●DYNAMIC-WORKFLOWS — Dynamic workflows are on by default on Max/Team and the API, for codebase-wide bug hunts and independent verification●ULTRACODE — Claude Code's new ultracode setting sits in the effort menu, fixing effort to xhigh while Claude decides when to run a workflow●OPUS4.8 — Claude Opus 4.8 is settled in as the default across major plans, with stronger coding, agentic, and reasoning skills
Claude Code × Next.js 15 App Router Production: RSC, Server Actions, Auth, Testing & Deployment
The practical guide to production Next.js 15 App Router development with Claude Code. Covers RSC architecture decisions, Server Actions patterns, Auth.js v5, Vitest testing, and Cloudflare Workers deployment with practical code examples.
Why Claude Code × Next.js 15 Transforms Production Development
Next.js 15's App Router, built around React Server Components (RSC), fundamentally changes how we think about web application architecture. It introduces powerful patterns for server-side rendering, data fetching, and streaming — but with that power comes increased complexity in managing server/client boundaries, cache invalidation strategies, and type safety across the stack.
Claude Code is the ideal pair programmer for navigating this complexity. Rather than just autocompleting code, it acts as a knowledgeable collaborator that understands your project's context, makes architectural recommendations, and generates production-ready implementations in seconds. In this guide, we'll walk through the real-world workflows and design patterns that make Claude Code + Next.js 15 such a powerful combination.
This guide is aimed at intermediate-to-advanced developers who already have a working knowledge of Next.js and TypeScript. We'll skip the basics and focus on the patterns that matter most in production.
Project Setup and Claude Code Configuration
Bootstrapping with the Right Defaults
Start with create-next-app and immediately configure Claude Code's project context:
Give Claude Code the following prompt to scaffold a production-ready setup in one shot:
Set up this Next.js 15 App Router project for production with:
- Drizzle ORM + PostgreSQL (type-safe DB layer)
- Auth.js v5 (authentication)
- Zod (runtime validation)
- Vitest (unit/component testing)
- Playwright (E2E testing)
- shadcn/ui (component library)
- Biome (fast ESLint + Prettier replacement)
Generate all configuration files and propose a src/ directory structure.
The CLAUDE.md File: Your Project's Persistent Context
Create CLAUDE.md at the project root. This file gives Claude Code the architectural constraints and conventions it needs to generate consistent, idiomatic code from day one:
# CLAUDE.md## Project OverviewNext.js 15 App Router + TypeScript + Cloudflare Workers deployment## Architecture Principles- Default to Server Components; use 'use client' only for interactive parts- Data fetching always happens in Server Components- Client components receive only the minimum props needed for interactivity- Server Actions handle all mutations and form submissions- Validate all user input with Zod before processing## Naming Conventions- Components: PascalCase (UserProfile.tsx)- Files: kebab-case (user-profile.tsx)- Types: PascalCase (UserType)- Server functions: verb-first (getUser, createPost, deleteComment)## Prohibited Patterns- Never use useState/useEffect for data fetching- Never call fetch() directly from client components- Never expose sensitive data through component props- No Node.js-specific APIs (fs, path, child_process) — Cloudflare Workers env
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Master RSC vs CSC design decisions and rapid implementation patterns with Claude Code guidance
✦Learn type-safe full-stack development combining Auth.js v5, Drizzle ORM, and Zod in a practical workflow
✦Understand the complete picture of production-quality testing and deployment automation with Vitest, Playwright, and Cloudflare Workers CI/CD
Secure payment via Stripe · Cancel anytime
App Router Architecture: Making RSC vs CSC Decisions
The Decision Framework
The most critical design decision in App Router development is deciding which components run on the server. When in doubt, ask Claude Code:
Help me decide: should each of these be RSC or CSC?
- User profile display (fetches from DB, no user interaction)
- Search form (shows real-time suggestions as user types)
- Product list (SSR initial load, client-side filtering)
- Like button (optimistic UI update + API mutation)
Here's the pattern in action:
// ✅ Server Component (default — no 'use client' needed)// src/app/profile/page.tsximport { db } from '@/lib/db'import { users } from '@/lib/schema'import { eq } from 'drizzle-orm'export default async function ProfilePage({ params,}: { params: Promise<{ id: string }>}) { const { id } = await params // Data fetching stays on the server — zero client-side bundle impact const user = await db.query.users.findFirst({ where: eq(users.id, id), columns: { id: true, name: true, email: true, createdAt: true, // passwordHash is never selected — can't accidentally leak it }, }) if (!user) return <div>User not found</div> return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> {/* Only the interactive part is a Client Component */} <FollowButton userId={user.id} /> </div> )}
Next.js 15 lets you stream UI progressively using Suspense boundaries, so slow data fetches never block fast ones:
// src/app/dashboard/page.tsximport { Suspense } from 'react'import { UserStats } from '@/components/user-stats'import { RecentActivity } from '@/components/recent-activity'import { Recommendations } from '@/components/recommendations'import { StatsSkeleton, ActivitySkeleton } from '@/components/skeletons'// Each component streams independently// A slow Recommendations fetch won't delay UserStats from renderingexport default function DashboardPage() { return ( <div className="grid grid-cols-3 gap-4"> <Suspense fallback={<StatsSkeleton />}> <UserStats /> </Suspense> <Suspense fallback={<ActivitySkeleton />}> <RecentActivity /> </Suspense> <Suspense fallback={<div>Loading recommendations...</div>}> <Recommendations /> </Suspense> </div> )}
Server Actions: Production-Grade Implementation Patterns
Type-Safe Server Actions with Zod
Server Actions are the backbone of App Router mutations. Combined with Zod and a discriminated union return type, they provide end-to-end type safety from the form to the database:
// src/app/actions/post.ts'use server'import { z } from 'zod'import { auth } from '@/lib/auth'import { db } from '@/lib/db'import { posts } from '@/lib/schema'import { revalidatePath } from 'next/cache'const CreatePostSchema = z.object({ title: z.string().min(1, 'Title is required').max(100, 'Max 100 characters'), content: z.string().min(10, 'Minimum 10 characters required'), published: z.boolean().default(false),})type ActionResult<T = void> = | { success: true; data: T } | { success: false; error: string; fieldErrors?: Record<string, string[]> }export async function createPost( formData: FormData): Promise<ActionResult<{ id: string }>> { const session = await auth() if (!session?.user?.id) { return { success: false, error: 'Authentication required' } } const result = CreatePostSchema.safeParse({ title: formData.get('title'), content: formData.get('content'), published: formData.get('published') === 'true', }) if (!result.success) { return { success: false, error: 'Validation failed', fieldErrors: result.error.flatten().fieldErrors, } } try { const [post] = await db.insert(posts).values({ ...result.data, authorId: session.user.id, createdAt: new Date(), updatedAt: new Date(), }).returning({ id: posts.id }) revalidatePath('/posts') if (result.data.published) revalidatePath('/') return { success: true, data: { id: post.id } } } catch (error) { console.error('Failed to create post:', error) return { success: false, error: 'Internal server error' } }}
Next.js automatically deduplicates identical fetch() calls within a single request. For ORM queries (which don't use fetch()), wrap them in React.cache():
// src/lib/queries.tsimport { cache } from 'react'import { db } from '@/lib/db'import { users, posts } from '@/lib/schema'import { eq } from 'drizzle-orm'// Call this from multiple Server Components in the same request tree —// only one DB query will executeexport const getUser = cache(async (id: string) => { return db.query.users.findFirst({ where: eq(users.id, id), })})export const getUserPosts = cache(async (userId: string) => { return db.query.posts.findMany({ where: eq(posts.authorId, userId), orderBy: (posts, { desc }) => [desc(posts.createdAt)], limit: 10, })})
Parallel Fetching with Promise.all
// src/app/user/[id]/page.tsximport { getUser, getUserPosts } from '@/lib/queries'import { notFound } from 'next/navigation'export default async function UserPage({ params,}: { params: Promise<{ id: string }>}) { const { id } = await params // Both queries fire simultaneously — no waterfall const [user, posts] = await Promise.all([ getUser(id), getUserPosts(id), ]) if (!user) notFound() return ( <div> <h1>{user.name}</h1> <PostList posts={posts} /> </div> )}
Cache Strategy Design
// src/app/products/page.tsx// Cache for 10 minutes (pricing doesn't change that often)export const revalidate = 600// For user-specific data that changes frequentlyexport const dynamic = 'force-dynamic'// Pre-generate static paths at build timeexport async function generateStaticParams() { const categories = await getCategories() return categories.map(cat => ({ category: cat.slug }))}
Auth.js v5 Authentication
Setup and Session Configuration
// src/lib/auth.tsimport NextAuth from 'next-auth'import GitHub from 'next-auth/providers/github'import Google from 'next-auth/providers/google'import { DrizzleAdapter } from '@auth/drizzle-adapter'import { db } from '@/lib/db'export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: DrizzleAdapter(db), providers: [ GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }), ], session: { strategy: 'database' }, callbacks: { // Extend session to include user ID and role session({ session, user }) { session.user.id = user.id session.user.role = user.role ?? 'user' return session }, },})
// src/app/api/auth/[...nextauth]/route.tsimport { handlers } from '@/lib/auth'export const { GET, POST } = handlers
Performance Optimization: PPR and Image Optimization
Partial Pre-rendering (PPR)
Partial Pre-rendering, stabilized in Next.js 15, lets you combine static and dynamic content within a single route. The static shell is cached at the edge and served instantly, while dynamic content streams in afterward. Claude Code can help you identify which parts of your pages are good candidates for PPR.
// src/app/products/[id]/page.tsximport { Suspense } from 'react'import { ProductDetails } from '@/components/product-details'import { PersonalizedRecommendations } from '@/components/personalized-recommendations'import { StaticProductInfo } from '@/components/static-product-info'// Static shell renders instantly from cache// Dynamic parts stream in via Suspenseexport default function ProductPage({ params,}: { params: Promise<{ id: string }>}) { return ( <div> {/* Static: rendered at build time, served from edge cache */} <StaticProductInfo productId={params} /> {/* Dynamic: real-time inventory, streams after static shell */} <Suspense fallback={<div>Loading inventory...</div>}> <ProductDetails productId={params} /> </Suspense> {/* Personalized: session-dependent, streams last */} <Suspense fallback={<div>Loading recommendations...</div>}> <PersonalizedRecommendations productId={params} /> </Suspense> </div> )}
Ask Claude Code to analyze any page and suggest the optimal PPR boundaries: "Review this page component and identify which parts should be static shell vs dynamic Suspense boundaries for best performance."
Image Optimization Best Practices
Next.js's <Image> component provides automatic format conversion (WebP/AVIF), lazy loading, and layout shift prevention. Here's the production pattern with Claude Code:
When your bundle grows large, ask Claude Code to help diagnose it:
# Install bundle analyzernpm install --save-dev @next/bundle-analyzer# Add to next.config.tsconst withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true',})# Run analysisANALYZE=true npm run build
Then share the bundle analysis output with Claude Code: "Here's my bundle analysis. Which dependencies are unusually large and what are the best alternatives or lazy-loading strategies?" Claude Code will give you specific recommendations for dynamic imports, tree-shaking improvements, and package alternatives.
Common Pitfalls and How Claude Code Helps You Avoid Them
Even experienced developers run into App Router gotchas. Here are the most common ones and how Claude Code helps you navigate them.
Pitfall 1: "use client" Directive Propagation
When you mark a component 'use client', every component it imports also becomes a client component — even if those imported components don't need to be. This can accidentally push large dependencies into the client bundle.
The fix is to push 'use client' as far down the component tree as possible. Ask Claude Code: "Review my component tree and identify where I should move the 'use client' boundary to minimize client bundle size."
// ❌ Problematic: wraps everything in client boundary'use client'import { HeavyChart } from 'heavy-chart-library' // pulled to clientimport { UserData } from '@/components/user-data' // pulled to clientexport function Dashboard() { const [filter, setFilter] = useState('weekly') return ( <> <UserData /> <HeavyChart filter={filter} onFilterChange={setFilter} /> </> )}// ✅ Fixed: isolate the interactive part// src/components/chart-controls.tsx'use client'export function ChartControls({ onFilterChange }: { onFilterChange: (f: string) => void }) { const [filter, setFilter] = useState('weekly') return ( <select onChange={e => onFilterChange(e.target.value)}> <option value="weekly">Weekly</option> <option value="monthly">Monthly</option> </select> )}// src/app/dashboard/page.tsx — no 'use client' neededimport { UserData } from '@/components/user-data' // stays as RSCimport { HeavyChart } from 'heavy-chart-library' // can use SSR versionimport { ChartControls } from '@/components/chart-controls' // only this is CSC
Pitfall 2: Serialization Errors with Server Component Props
You can only pass serializable data from Server Components to Client Components. Functions, class instances, dates (in some contexts), and undefined values can cause serialization errors that are difficult to debug.
// ❌ Cannot pass a Date object directly — serialization error<ClientComponent date={new Date()} />// ✅ Serialize before passing<ClientComponent dateIso={new Date().toISOString()} />// ❌ Cannot pass a function from RSC to CSC props (it won't serialize)<ClientComponent onAction={() => db.doSomething()} />// ✅ Use a Server Action insteadimport { doAction } from '@/app/actions'<ClientComponent onAction={doAction} /> // Server Actions are serializable references
When you hit a serialization error, paste the error into Claude Code with context about your component structure. It will identify exactly which prop is causing the issue and suggest the correct fix.
Pitfall 3: Cache Invalidation Mismatches
revalidatePath() and revalidateTag() are powerful but easy to misuse. If you call revalidatePath('/posts') but your actual pages are at /posts/[slug], the root path revalidation may not cascade as expected.
// ✅ Revalidate both the list and the specific itemexport async function updatePost(id: string, data: PostUpdateData) { await db.update(posts).set(data).where(eq(posts.id, id)) // Revalidate the specific post page revalidatePath(`/posts/${id}`) // Revalidate the post list revalidatePath('/posts') // Revalidate homepage if featured posts appear there revalidatePath('/')}// Better: use tags for fine-grained controlexport async function updatePost(id: string, data: PostUpdateData) { await db.update(posts).set(data).where(eq(posts.id, id)) // Revalidate all pages tagged with this post's ID revalidateTag(`post-${id}`) revalidateTag('posts-list')}
Ask Claude Code to audit your revalidatePath/revalidateTag calls: "Review all the Server Actions in this file and verify the cache revalidation strategy is correct and complete."
Pitfall 4: Missing Loading UI for Nested Layouts
Nested layouts in App Router can create unexpected loading states if you don't have loading.tsx files at each level. Users may see a blank screen or partial content flash.
Claude Code can generate appropriate skeleton components for each level: "Generate a loading.tsx component that matches the layout structure of my dashboard page with appropriate skeleton placeholders."
Pitfall 5: TypeScript Types for Dynamic Route Params
In Next.js 15, route params are now Promises that must be awaited. This is a breaking change from Next.js 14 that catches many developers off guard.
// ❌ Next.js 14 style — breaks in Next.js 15export default function Page({ params }: { params: { id: string } }) { // params.id works synchronously in 14, but causes type errors in 15 return <div>{params.id}</div>}// ✅ Next.js 15 styleexport default async function Page({ params,}: { params: Promise<{ id: string }>}) { const { id } = await params return <div>{id}</div>}
If you're migrating from Next.js 14 to 15, ask Claude Code: "Update all page and layout components in this directory to use the Next.js 15 async params pattern." It will systematically update every affected file.
Drizzle ORM Integration: Type-Safe Database Layer
Schema Definition and Migrations
Drizzle ORM is the recommended database layer for Next.js 15 production apps — it's fully TypeScript-native, works in edge environments, and has excellent Cloudflare D1 support.
// src/lib/db.tsimport { drizzle } from 'drizzle-orm/node-postgres'import { Pool } from 'pg'import * as schema from './schema'const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10,})export const db = drizzle(pool, { schema })
When you need complex queries, describe what you want in plain language to Claude Code and it will generate the optimal Drizzle query:
Write a Drizzle ORM query that:
- Gets all published posts with their author's name
- Filters by category if provided
- Supports cursor-based pagination (after postId)
- Returns only fields needed for the post list view
- Orders by most recent first
Looking back
Claude Code and Next.js 15 App Router combine into a genuinely powerful production development stack. The key takeaways:
RSC/CSC boundary: Keep data fetching server-side by default; pass only interactivity-required props to client components
Server Actions: Use the ActionResult<T> discriminated union with Zod for end-to-end type safety
Data fetching: Eliminate waterfalls with Promise.all and React.cache() memoization
Testing: Two-layer approach — Vitest for unit/component, Playwright for E2E
Deployment: GitHub Actions + Cloudflare Workers for zero-touch production deploys
Claude Code's real value isn't just knowing these patterns — it's being able to apply them correctly and consistently in your actual codebase. Set up CLAUDE.md with your project's constraints, be specific about what you're building, and treat it as the senior engineering partner it's designed to be.
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.