Agentic Coding

AI-Assisted Next.js Development

Build Next.js App Router projects faster with AI assistance: scaffold routes, generate Server Components, write tests, and debug type errors using Claude Code and Cursor.

May 31, 2026 10 min read
AI-Assisted Next.js Development

AI-assisted Next.js development works best when you accept that the App Router has genuinely new conventions, and that most AI tools were trained on Next.js Pages Router patterns. Without explicit guidance, they'll generate getServerSideProps, client-side data fetching in useEffect, and _app.tsx files that don't exist in the App Router. The fix isn't to avoid AI tools; it's to give them accurate context about which version and which conventions your project uses.

At Laxaar, we've built production Next.js App Router projects with AI-assisted workflows. The pattern that works: spend 30 minutes on configuration upfront, then let the tools accelerate scaffolding, component generation, and test writing throughout the project. This tutorial shows exactly how.

Prerequisites: Next.js 15 project with App Router, TypeScript strict mode, Node.js 20+. We'll use Claude Code CLI (npm install -g @anthropic-ai/claude-code) and Cursor 0.45+.

What you'll build

Step 1: Configure for App Router

The most important thing you'll do in this entire workflow is write an accurate CLAUDE.md. Every prompt you send to Claude Code or Cursor without this context is a coin flip between App Router and Pages Router output.

Create CLAUDE.md at your Next.js project root:

# Next.js project context

Next.js version: 15 (App Router). Do NOT use Pages Router conventions.

## File conventions (App Router)
- `app/page.tsx` — route page component (async by default, Server Component)
- `app/layout.tsx` — shared layout (async, Server Component)
- `app/loading.tsx` — Suspense fallback for the route segment
- `app/error.tsx` — error boundary (must be Client Component: `"use client"`)
- `app/not-found.tsx` — 404 page for the segment
- API routes: `app/api/[endpoint]/route.ts`, export named functions GET, POST, etc.
- Server actions: async functions with `"use server"` directive at the top

## Component rules
- Default to Server Components (no directive needed).
- Add `"use client"` ONLY when the component uses: useState, useEffect, useRef, event listeners, browser APIs.
- Never use `getServerSideProps`, `getStaticProps`, or `getInitialProps` — those are Pages Router.
- Data fetching: use async/await directly in Server Components. No useEffect for data.

## Styling
- Tailwind CSS. No CSS modules unless component needs keyframe animations.

## TypeScript
- Strict mode is on. No implicit `any`.
- For route params: `{ params }: { params: Promise<{ slug: string }> }` — params is a Promise in Next.js 15.

## After every file edit
Run: `npm run build` or `npx tsc --noEmit` to catch errors.

The params-as-Promise note is a Next.js 15 breaking change that every AI tool gets wrong without explicit instruction. Add it prominently.

For Cursor, mirror the same content in .cursor/rules. Keep them in sync.

Step 2: Scaffold routes and layouts

Scaffolding a nested route with layout in Next.js App Router by hand is tedious but pattern-driven. AI handles it well with the right context.

claude "Create a /dashboard section with the following App Router structure:
- app/(dashboard)/layout.tsx — shared sidebar layout. Read app/layout.tsx first to understand the root layout pattern.
- app/(dashboard)/dashboard/page.tsx — dashboard home page, shows a welcome heading and a placeholder stats grid
- app/(dashboard)/settings/page.tsx — settings page with a placeholder form
- app/(dashboard)/settings/profile/page.tsx — nested profile settings page

All pages are Server Components. Use Tailwind for layout. After creating each file, run npx tsc --noEmit and fix any type errors."

The route group (dashboard) keeps the URL clean (/dashboard, /settings) while sharing the sidebar layout. The agent should know this if you've noted it in CLAUDE.md, but if it creates app/dashboard/layout.tsx instead of app/(dashboard)/layout.tsx, correct it explicitly:

claude "Move the layout from app/dashboard/layout.tsx to app/(dashboard)/layout.tsx — 
use a route group so the URL doesn't include 'dashboard' as a segment. 
Update any imports if needed."

Expected file structure after this step:

app/
  (dashboard)/
    layout.tsx        # sidebar + main area
    dashboard/
      page.tsx
    settings/
      page.tsx
      profile/
        page.tsx

Step 3: Server and Client Components

Getting the Server/Client boundary right is the area where AI assistance most often fails in Next.js App Router projects. The rule is simple but consistently violated: if a component needs interactivity (click handlers, state, browser APIs), it's a Client Component. Everything else is a Server Component.

Have the agent generate a data table component that's interactive:

claude "Create a reusable DataTable component at components/DataTable.tsx.
It should:
- Accept a generic 'data' prop (array of objects) and a 'columns' prop (array of {key, header})
- Render an HTML table with Tailwind classes
- Include client-side sorting when a column header is clicked
- Include a search input that filters rows client-side

This component needs interactivity, so it must be a Client Component.
After creating it, show me how to use it in a Server Component page."

The agent should produce a file that starts with "use client" and uses useState for sort state and filter text. Then, to show the Server/Client boundary in practice:

// app/(dashboard)/dashboard/page.tsx — Server Component
import { DataTable } from "@/components/DataTable";
import { db } from "@/lib/db";

export default async function DashboardPage() {
  // Data fetching happens here, on the server
  const projects = await db.project.findMany({ orderBy: { createdAt: "desc" } });

  const columns = [
    { key: "name", header: "Name" },
    { key: "status", header: "Status" },
    { key: "createdAt", header: "Created" },
  ];

  return (
    <main className="p-6">
      <h1 className="text-2xl font-semibold mb-4">Projects</h1>
      {/* DataTable is a Client Component — receives serializable props from the server */}
      <DataTable data={projects} columns={columns} />
    </main>
  );
}

This pattern (Server Component fetches data, passes serializable props to a Client Component) is the right mental model for App Router. The AI will produce it correctly when the context in CLAUDE.md is accurate.

Step 4: Data fetching

Next.js 15 App Router data fetching is straightforward in concept: await the database or API call directly in the async Server Component. Where AI tools struggle is with caching, revalidation, and parallel fetching.

claude "In app/(dashboard)/dashboard/page.tsx, improve the data fetching:
1. Fetch projects and recent activity in parallel using Promise.all (not sequentially)
2. Add error handling — if the db call fails, throw an error that the nearest error.tsx will catch
3. Add a loading.tsx file at app/(dashboard)/dashboard/loading.tsx with a skeleton UI

Read the existing page.tsx first. After changes, run npx tsc --noEmit."

The agent should produce parallel fetching like this:

// Parallel — correct
const [projects, activity] = await Promise.all([
  db.project.findMany({ orderBy: { createdAt: "desc" }, take: 10 }),
  db.activityLog.findMany({ orderBy: { createdAt: "desc" }, take: 5 }),
]);

Not sequential like this:

// Sequential — wrong, unnecessarily slow
const projects = await db.project.findMany(...)
const activity = await db.activityLog.findMany(...)

If the agent produces sequential fetching, correct it explicitly. Sequential is the path of least resistance for AI tools because it's simpler to generate, but it doubles your page load time for no reason.

For the loading.tsx:

// app/(dashboard)/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <main className="p-6">
      <div className="h-8 w-48 bg-gray-200 rounded animate-pulse mb-4" />
      <div className="space-y-3">
        {Array.from({ length: 5 }).map((_, i) => (
          <div key={i} className="h-12 bg-gray-100 rounded animate-pulse" />
        ))}
      </div>
    </main>
  );
}

Simple, correct, and demonstrates the Suspense streaming pattern. The skeleton shows while the async page component resolves.

Step 5: Testing

Testing Next.js App Router components requires different setup than Pages Router. Server Components can't be rendered in jsdom; you test them by calling them as async functions. Client Components render normally with React Testing Library.

claude "Set up testing for this project using Vitest and React Testing Library.
1. Install required packages: vitest, @vitejs/plugin-react, @testing-library/react, @testing-library/jest-dom, jsdom
2. Create vitest.config.ts at the project root
3. Write a test for the DataTable component at components/DataTable.test.tsx — cover: renders columns, sorts on header click, filters on search input
4. Write a test for the /dashboard page — call the page function directly as an async function, mock the db calls, assert on the returned JSX structure

Show the test for each before creating the file."

The config file for Next.js + Vitest:

// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    globals: true,
    setupFiles: ["./vitest.setup.ts"],
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./"),
    },
  },
});

For the Server Component page test:

// app/(dashboard)/dashboard/page.test.tsx
import { describe, it, expect, vi } from "vitest";
import DashboardPage from "./page";

// Mock the db module
vi.mock("@/lib/db", () => ({
  db: {
    project: {
      findMany: vi.fn().mockResolvedValue([
        { id: "1", name: "Test Project", status: "ACTIVE", createdAt: new Date() },
      ]),
    },
    activityLog: {
      findMany: vi.fn().mockResolvedValue([]),
    },
  },
}));

describe("DashboardPage", () => {
  it("fetches and returns projects data", async () => {
    const result = await DashboardPage();
    // Server Component returns JSX — check it's valid
    expect(result).toBeTruthy();
    expect(result.type).toBe("main");
  });
});

Run the tests:

npx vitest run

Expect the DataTable test to be more involved. The agent should produce tests that actually interact with the sort and filter behavior using fireEvent from Testing Library, not just verify that the component renders.

Common pitfalls

AI generating Pages Router code. Without explicit context, Claude Code and Cursor will often default to getServerSideProps or useEffect for data fetching. The CLAUDE.md file prevents this, but always scan generated files for getServerSideProps, getStaticProps, and _app references before accepting them.

Forgetting that params is a Promise in Next.js 15. In Next.js 15, route params are typed as Promise<{ slug: string }>, not { slug: string }. You need to await params before accessing its properties. AI tools almost universally get this wrong without explicit instruction. Add it to your CLAUDE.md and double-check generated page files.

Using Client Components when Server Components would work. AI tools sometimes add "use client" preemptively "just in case." Remove it unless the component actually needs browser APIs or React state. Unnecessary Client Components move data fetching to the browser and lose Server Component benefits.

Not testing the Server/Client boundary. If a Server Component accidentally imports a Client Component that imports a Node.js-only module, you'll get a build error. Test both component types in your CI pipeline. Don't wait for a deployment failure.

Frequently Asked Questions

Does AI work well with Next.js 15's new features like partial prerendering?

Partially. AI tools know about partial prerendering conceptually, but correct <Suspense> placement and unstable_noStore() usage require careful review. Use AI to generate the structure, then verify the caching behavior manually with next build output and the Network tab.

Can I use Claude Code to upgrade a Pages Router project to App Router?

Yes, but treat it as an assisted migration, not an automated one. Have Claude Code migrate one route at a time: read the Pages Router file, generate the App Router equivalent, run the build, fix errors. Don't try to migrate the whole project in one prompt. The context gets too large and error recovery becomes expensive.

What's the best way to handle environment variables in AI-generated Next.js code?

Add a section to CLAUDE.md listing your env variables and their names: NEXT_PUBLIC_API_URL for client-accessible vars, DATABASE_URL for server-only. AI tools will use the right names if you tell them. Without this, they'll invent API_KEY, DB_URL, and other names that don't match your .env.local.

How do I prevent AI from generating server actions that skip validation?

Add an explicit rule to CLAUDE.md: "All server actions must validate input with zod before processing. Never trust raw FormData or JSON body without parsing and validating the schema." This applies consistently once it's in the context file.

Is AI assistance worth it for small Next.js projects?

The payoff is smaller on projects under 3-4 weeks of work, because the configuration overhead takes a day or two to pay back. For anything larger, the ROI is clear. The Laxaar team sees the biggest gains on projects with repetitive patterns: multi-page dashboards, CRUD-heavy admin interfaces, and projects with a large test surface.

The Laxaar team builds and ships Next.js products for clients who need expert engineering without building a full team. If you want production-quality Next.js work with AI-assisted velocity, talk to us about your project or see what we've built.

Next.jsAI-Assisted CodingAgentic Coding
Grow your business with us

Take your business to the next level.

Tell us what you're building. We'll come back inside one business day with a fixed scope, timeline, and team — or an honest “this isn't a fit”.

ENGINEERING PHILOSOPHY

Code is useless if it's not comprehensible to those who maintain it. We write code the next person can actually understand.