← All posts
·11 min read

Claude Code with Next.js: App Router Workflow

Claude CodeNext.jsWorkflowSetup
Claude Code with Next.js: App Router Workflow

Why App Router requires explicit Claude Code configuration

Next.js App Router introduced a fundamental split in how components work. Server components run on the server, have direct database access, and cannot use browser APIs or React state. Client components run in the browser, can use hooks and event handlers, and cannot make direct server calls. These two worlds interleave in the same file tree, and the rules for which component type to use are not always obvious.

Without configuration, Claude Code applies this distinction inconsistently. It adds "use client" to components that do not need it, which bloats the client bundle. It writes data fetching inside useEffect when it should be in a server component. It creates API routes for operations that could be server actions. Every one of these is a working implementation, but none of them is the right implementation for a project using App Router correctly.

A project-specific CLAUDE.md encodes the decision rules once and applies them consistently. This guide covers that configuration, the server-versus-client component decision process Claude Code uses, server actions for mutations, caching strategy, and the Playwright testing workflow. If you are new to Claude Code, the Claude Code setup guide covers installation before any of this applies.

The Next.js CLAUDE.md

A Next.js CLAUDE.md needs to answer more questions than a standard React config because App Router adds layers of decision-making that Claude Code must navigate correctly from the start.

# Next.js project rules

## App Router conventions
- Default: Server Component. Add "use client" only when the component needs:
  state (useState, useReducer), browser APIs, event handlers, or third-party
  client libraries that do not support server rendering.
- No "use client" on layout.tsx files unless unavoidable.
- Co-locate page-level data fetching in the page.tsx component, not in children.
- Use React.cache() for data fetching functions shared across a request.

## File structure
- App dir: app/ (Next.js routing)
- Shared components: components/ (outside app/)
- Server-only utilities: lib/server/ (import 'server-only' at top of each file)
- Client-only utilities: lib/client/ (import 'client-only' at top of each file)
- Types: types/{domain}.ts
- Path alias: @/ maps to ./

## Data fetching
- Server Components: fetch() with Next.js cache tags, or direct ORM calls
- Client Components: TanStack Query for remote data. No fetch() in useEffect.
- Mutations: Server Actions. Not API routes unless external access is required.
- Revalidation: revalidateTag() in server actions after successful mutations

## Server Actions
- Defined in app/actions/{domain}.ts with "use server" directive
- Always validate input with Zod before touching the database
- Return a discriminated union: { success: true, data } | { success: false, error }
- Call revalidateTag() for affected cache tags after every successful mutation

## Database
- Prisma for ORM. Client initialized in lib/db.ts (singleton pattern).
- Never import db.ts in client components.
- Use Prisma.$transaction for multi-step writes.

## Testing
- Framework: Playwright for E2E, Vitest + React Testing Library for units
- Run E2E: `npx playwright test`
- Run unit: `npx vitest run`
- Test files: __tests__/ at the app root for E2E, colocated for unit tests

## Hard rules
- Never fetch data in a Client Component that could be fetched in its Server parent.
- Never import server-only code (db, server env vars) in client components.
- Run `npx next build` to verify after structural changes.
- No hardcoded environment variables. Use process.env with validation in lib/env.ts.

The server-only and client-only package imports are particularly valuable. They make boundary violations into build errors rather than silent runtime issues. Claude Code respects this pattern when it is documented in the CLAUDE.md.

Server components vs client components: the decision process

The most important configuration you can give Claude Code for a Next.js project is a clear decision rule for when to add "use client". Without it, Claude defaults to adding the directive whenever it sees a pattern that could conceivably need browser access, which means every interactive-looking component ends up as a client component regardless of whether it actually needs to be.

The decision rule Claude Code applies when your CLAUDE.md is correctly configured:

Server Component when: the component fetches data, reads from the database, accesses server-only environment variables, performs authentication checks, or is purely presentational with no interactivity.

Client Component when: the component uses useState or useReducer, attaches event handlers (onClick, onChange, onSubmit), uses browser APIs (window, localStorage, navigator), uses hooks that require browser context (useEffect, useLayoutEffect, useCallback with DOM refs), or wraps a third-party library that does not support server rendering.

The productive pattern is to push "use client" as far down the component tree as possible. A page that displays a product list with a filter component at the top should have the page and list as Server Components and only the filter as a Client Component. The filter receives the data as a prop from the server-rendered parent and handles user interaction locally.

Ask Claude Code to apply this explicitly when starting a new feature:

"We are building a product listing page with server-rendered product cards and a client-side filter panel. Structure the component tree so only the filter component is a Client Component. The rest of the page should be server-rendered."

This framing produces the right structure. "Build a product listing page with filters" does not, without the CLAUDE.md rules backing it up.

Server actions for mutations

Server Actions are the correct pattern for form submissions and data mutations in App Router projects. They run on the server, have direct database access, and eliminate the boilerplate of an API route for operations that are not accessed externally. Claude Code handles them well when you specify the pattern.

The prompt that produces consistent output:

"Create a server action updateProductPrice in app/actions/products.ts. It receives a productId and newPrice. Validate with Zod (price must be a positive number). Update the product in the database using Prisma. Call revalidateTag('products') after a successful update. Return { success: true, data: updatedProduct } or { success: false, error: string }."

Claude Code generates the action with the "use server" directive, the Zod schema, the database call, the cache invalidation, and the discriminated union return type. The key additions your CLAUDE.md contributes here are the return type shape and the revalidation call, which Claude omits without explicit instruction.

On the client side, consuming a server action with optimistic updates:

"Create a PriceUpdateForm client component that calls updateProductPrice. Show optimistic UI while the action is pending using useTransition. Display validation errors from the server response below the input."

The useTransition pattern is the correct way to handle loading state with server actions. Claude Code knows this, but names it explicitly in your prompt to ensure the right pattern is selected over the alternatives.

When to use API routes instead

Server actions are not always the right choice. Use an API route when:

  • The mutation needs to be called from an external service (webhook handler, mobile app, third-party integration)
  • The endpoint needs custom HTTP headers or status codes
  • The operation needs to stream a response

Add this distinction to your CLAUDE.md so Claude Code does not reach for a server action in these cases:

- Server Actions: internal form submissions and mutations from this app only
- API Routes (app/api/): external access, webhooks, streaming responses, custom headers

Data fetching and caching strategy

App Router's fetch caching model is one of the most powerful features in Next.js and one of the most commonly misunderstood. Without guidance, Claude Code produces fetch calls with either no caching configuration (meaning no revalidation) or no-store everywhere (meaning no caching). Neither is correct for most production applications.

The configuration that produces sensible defaults:

## Caching strategy

- Static data (product catalog, blog posts): fetch with revalidate: 3600
- User-specific data: fetch with cache: 'no-store' or cookies() / headers() dependency
- After mutations: revalidateTag('{domain}') to bust specific cache segments
- Tag all fetches: { next: { tags: ['{domain}'] } } so revalidation is targeted

Example:
  const products = await fetch('/api/products', {
    next: { tags: ['products'], revalidate: 3600 }
  });

With these rules in place, Claude Code generates fetch calls with appropriate cache tags automatically. The tags connect the data-fetching layer to the server action revalidation calls, so mutations invalidate exactly the right cached segments without busting the entire cache.

For direct database calls in Server Components (via Prisma), the caching layer is React.cache(). Ask Claude Code to wrap shared data-fetching functions:

"Wrap the getProductById function in React.cache() so it is deduplicated within a single request when called from multiple server components."

Testing Next.js applications

Testing is the biggest gap in most Claude Code with Next.js workflows. Server Components cannot be rendered in a standard Jest/Vitest environment, which means traditional unit tests do not work on the server-rendering layer. The correct split is Playwright for E2E tests that cover the full stack, and Vitest for Client Component unit tests and utility functions.

Your CLAUDE.md should encode this split explicitly:

## Test scope

- Playwright: user-facing flows (login, checkout, navigation, form submission)
- Vitest: Client Components (rendering, user interaction), utility functions, Zod schemas
- Do not write Vitest tests for Server Components. Test them via Playwright E2E.
- Server Actions: test via Playwright form submissions, not direct function calls

The Playwright configuration that works well with App Router:

// playwright.config.ts
export default defineConfig({
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
  use: {
    baseURL: 'http://localhost:3000',
  },
});

Ask Claude Code to generate Playwright tests as part of feature development, not as a separate step:

"After building the checkout flow, write Playwright tests that cover: adding an item to the cart, proceeding to checkout, filling the payment form, and verifying the confirmation page renders."

Claude Code generates tests that use page.goto, page.fill, page.click, and page.waitForURL correctly. The tests exercise the full stack including server actions, which means they catch boundary violations and data flow issues that unit tests would miss entirely.

The Claude Code testing guide covers the broader testing workflow for projects where server-rendered components are involved.

Debugging App Router issues

The most common failure mode when using Claude Code with Next.js App Router is a boundary violation: importing a server-only module into a client component or using a client-only API in a server component. These errors surface in three ways.

Build errors are the easiest. next build catches most violations. The error message points to the exact import. Paste the full build output to Claude Code and ask it to trace the import chain and fix the violation.

Runtime errors in development are harder because Next.js sometimes renders server components on the client during hydration in ways that defer the error. If you see a "Cannot read properties of undefined" that does not have a clear line number, ask Claude Code:

"This runtime error appears in the browser console: [paste error]. The component tree for this page is: [paste structure]. Check for server-only imports in client components and fix the boundary violation."

Silent failures are the hardest. A component that imports db.ts in a client component may not throw an error in development if the database client happens to be initialized in a way that does not immediately fail on the client side. The server-only package prevents this by converting the silent failure into a build error.

For debugging approaches that work across Claude Code workflows, the Claude Code debugging guide covers the full methodology.

The build verification loop

One workflow pattern that catches issues early: run npx next build after any structural change and let Claude Code interpret the output.

Add this to your CLAUDE.md:

## Build verification

After any of these changes, run `npx next build` and fix all errors before
the task is complete:
- Adding or removing "use client" directives
- Moving components between server and client boundaries
- Adding new imports to any component
- Creating or modifying server actions
- Changing the app/ directory structure

The Next.js build catches type errors, boundary violations, missing environment variables, and invalid route configurations that the development server sometimes defers. Claude Code iterates on build errors automatically when the rule is explicit. Without it, Claude considers a task complete when the dev server is running, which misses a category of errors that only surface at build time.

This is the same principle behind Claude Code hooks: defining verification steps that run automatically rather than relying on manual review after every change.

What Next.js developers get wrong first

Three patterns appear consistently when developers start using Claude Code with App Router.

Over-using "use client". Without a clear decision rule, Claude Code adds "use client" to any component that contains an event handler, even when the event handler could be lifted to a small client wrapper. The result is server-rendered pages where the entire component tree is client-rendered. The CLAUDE.md rule about pushing "use client" as far down the tree as possible prevents this.

Writing data fetching in useEffect. Developers coming from Pages Router or plain React have years of muscle memory for useEffect + fetch. Claude Code mirrors this pattern when it appears in the codebase or is not explicitly countered in CLAUDE.md. The rule "Server Components: fetch directly. Client Components: TanStack Query. No fetch() in useEffect." eliminates this pattern.

Skipping the build step. Development mode is forgiving. It hot-reloads boundary violations, defers type errors, and sometimes masks import issues that production builds catch. The build verification loop above fixes this. It takes 30 seconds to add to your CLAUDE.md and prevents an entire category of production surprises.

Getting more from your Next.js workflow

The configuration in this guide produces a Claude Code setup where App Router's server-versus-client split is applied consistently, server actions handle mutations with proper validation and cache invalidation, and Playwright tests cover the full stack rather than stopping at the component boundary.

The starting point is the CLAUDE.md template above. Add it before your first Claude Code session on a new Next.js project. Run one full feature cycle (page, server action, client form, Playwright test) and adjust the rules based on what Claude generates. Two or three additional rules typically emerge from the first session. After that, the output is consistent enough to ship with confidence.

The Claude Code best practices guide covers the session management and context control techniques that keep performance sharp on larger App Router codebases. The Claude Code memory systems guide explains how to extend the CLAUDE.md pattern across multiple projects without duplicating configuration.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir