| name | Next.js App Router Development |
| description | Build Next.js applications with App Router, Server and Client Components, data fetching, routing, and TypeScript. Use when implementing Next.js pages, layouts, API routes, or React Server Components. |
| tools | Read, Grep, Glob, Write, Edit, Bash |
| model | inherit |
Skill: Next.js App Router Development
Complete guide for building Next.js applications using the App Router architecture with Server and Client Components, data fetching, routing, and navigation.
When to Use
- Creating new pages or layouts in Next.js App Router
- Implementing Server and Client Component patterns
- Setting up data fetching with Server Components
- Building API route handlers
- Implementing navigation and routing
- Debugging Next.js-specific issues
- Working with dynamic routes and params
Domain Knowledge
Critical Patterns
Server vs Client Components (CRITICAL)
In Next.js App Router, components are Server Components by default.
Server Components:
- Default behavior (no directive needed)
- Render on server only
- Can be async for data fetching
- Cannot use React hooks (useState, useEffect, etc.)
- Cannot use browser APIs
- Better performance (smaller bundle size)
Client Components:
- Require
"use client" directive at top of file
- Render on both server and client
- Can use React hooks (useState, useEffect, etc.)
- Can use browser APIs
- Required for interactivity
async function ServerPage() {
const data = await fetchData();
return <div>{data}</div>;
}
"use client";
import { useState } from "react";
function ClientComponent() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Rule: Use Server Components by default, only use Client Components when you need:
- React hooks (useState, useEffect, etc.)
- Event handlers (onClick, onChange, etc.)
- Browser APIs (window, localStorage, etc.)
- Third-party libraries that use hooks
Async Server Components for Data Fetching
Server Components can be async, enabling direct data fetching:
async function UserProfile({ userId }: { userId: string }) {
const user = await fetchUser(userId);
return <div>{user.name}</div>;
}
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
Why: Server Components don't support hooks. Use async/await directly instead.
Server to Client Data Flow
Pass data from Server Components to Client Components via props:
async function Page() {
const data = await fetchData();
return <ClientComponent data={data} />;
}
"use client";
function ClientComponent({ data }: { data: Data }) {
const [selected, setSelected] = useState(null);
return (
<div onClick={() => setSelected(data.id)}>
{data.name}
</div>
);
}
Rule: Server Components fetch data, Client Components handle interactivity.
Navigation Hooks (Client Only)
Navigation hooks can ONLY be used in Client Components:
"use client";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
function Navigation() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
return (
<button onClick={() => router.push("/dashboard")}>
Go to Dashboard
</button>
);
}
Common imports:
- Import from
"next/navigation" (NOT "next/router")
useRouter - programmatic navigation
usePathname - current path
useSearchParams - URL query params
Awaiting Params in Next.js 15+ (CRITICAL)
In Next.js 15+, dynamic route params and searchParams MUST be awaited:
export default function Page({ params }: { params: { id: string } }) {
return <div>Post {params.id}</div>;
}
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <div>Post {id}</div>;
}
searchParams also need awaiting:
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ query?: string }>;
}) {
const { query } = await searchParams;
return <div>Search: {query}</div>;
}
Why: Next.js 15 made params async for better streaming and performance.
Key Files
- app/layout.tsx - Root layout (wraps all pages)
- app/page.tsx - Home page
- app/[dynamic]/page.tsx - Dynamic route page
- app/api/[route]/route.ts - API route handler
- middleware.ts - Middleware for auth, redirects, etc.
- next.config.ts - Next.js configuration
File-Based Routing
app/
├── page.tsx → /
├── about/page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/page.tsx → /blog/[slug]
├── dashboard/
│ ├── layout.tsx → Layout for /dashboard/*
│ └── page.tsx → /dashboard
└── api/
└── users/route.ts → /api/users
Special Files:
page.tsx - Route page
layout.tsx - Shared layout
loading.tsx - Loading UI
error.tsx - Error UI
not-found.tsx - 404 UI
route.ts - API endpoint
Workflows
Workflow 1: Create New Page with Data Fetching
Step 1: Create Page File
import { api } from "@/convex/_generated/api";
import { fetchQuery } from "convex/nextjs";
export default async function PostsPage() {
const posts = await fetchQuery(api.posts.list);
return (
<div>
<h1>Posts</h1>
<PostList posts={posts} />
</div>
);
}
Step 2: Create Client Component for Interactivity
"use client";
import { useState } from "react";
export function PostList({ posts }: { posts: Post[] }) {
const [filter, setFilter] = useState("");
const filtered = posts.filter(p =>
p.title.toLowerCase().includes(filter.toLowerCase())
);
return (
<div>
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter posts..."
/>
{filtered.map(post => (
<PostCard key={post._id} post={post} />
))}
</div>
);
}
Step 3: Add Loading State
export default function Loading() {
return <div>Loading posts...</div>;
}
Step 4: Add Error Handling
"use client";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
Workflow 2: Create Dynamic Route
Step 1: Create Dynamic Route File
import { api } from "@/convex/_generated/api";
import { fetchQuery } from "convex/nextjs";
import { Id } from "@/convex/_generated/dataModel";
export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const post = await fetchQuery(api.posts.get, {
id: id as Id<"posts">
});
if (!post) {
return <div>Post not found</div>;
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
Step 2: Generate Static Params (Optional)
For static site generation, provide all possible params:
export async function generateStaticParams() {
const posts = await fetchQuery(api.posts.list);
return posts.map((post) => ({
id: post._id,
}));
}
Step 3: Add Metadata
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const post = await fetchQuery(api.posts.get, { id: id as Id<"posts"> });
return {
title: post.title,
description: post.excerpt,
};
}
Workflow 3: Create API Route Handler
Step 1: Create Route File
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("query");
const users = await fetchUsers(query);
return NextResponse.json({ users });
} catch (error) {
return NextResponse.json(
{ error: "Failed to fetch users" },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
if (!body.name || !body.email) {
return NextResponse.json(
{ error: "Name and email required" },
{ status: 400 }
);
}
const user = await createUser(body);
return NextResponse.json({ user }, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: "Failed to create user" },
{ status: 500 }
);
}
}
Step 2: Use Route from Frontend
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email }),
});
const { user } = await response.json();
Workflow 4: Implement Protected Route
Step 1: Create Middleware
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isProtectedRoute = createRouteMatcher([
"/dashboard(.*)",
"/api/protected(.*)",
]);
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) auth().protect();
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
Step 2: Access User in Server Component
import { auth } from "@clerk/nextjs/server";
export default async function Dashboard() {
const { userId } = await auth();
if (!userId) {
redirect("/sign-in");
}
const userData = await fetchUserData(userId);
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {userData.name}</p>
</div>
);
}
Step 3: Access User in Client Component
"use client";
import { useUser } from "@clerk/nextjs";
export function UserProfile() {
const { user } = useUser();
if (!user) return null;
return (
<div>
<img src={user.imageUrl} alt={user.fullName} />
<p>{user.emailAddresses[0].emailAddress}</p>
</div>
);
}
Workflow 5: Implement Navigation
Step 1: Use Link Component for Navigation
import Link from "next/link";
export function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
</nav>
);
}
Step 2: Programmatic Navigation in Client Component
"use client";
import { useRouter } from "next/navigation";
export function LoginForm() {
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login();
router.push("/dashboard");
};
return <form onSubmit={handleSubmit}>...</form>;
}
Step 3: Access Current Route Information
"use client";
import { usePathname, useSearchParams } from "next/navigation";
export function ActiveLink({ href, children }) {
const pathname = usePathname();
const isActive = pathname === href;
return (
<Link
href={href}
className={isActive ? "active" : ""}
>
{children}
</Link>
);
}
Troubleshooting
Issue: Cannot Use useState/useEffect in Component
Symptoms:
- Error: "You're importing a component that needs useState/useEffect"
- Hooks not working in component
Cause: Component is a Server Component (default), which doesn't support React hooks
Solution:
Add "use client" directive at the top of the file:
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Frequency: High (very common mistake)
Issue: Navigation Hooks Not Found
Symptoms:
- Error: "useRouter not found"
- Import error for navigation hooks
Cause: Importing from wrong package or using in Server Component
Solution:
- Import from
"next/navigation" (NOT "next/router"):
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
- Ensure component is a Client Component:
"use client";
import { useRouter } from "next/navigation";
Frequency: Medium
Issue: Params is Promise Error
Symptoms:
- Error: "Type 'Promise<{...}>' is not assignable to type '{...}'"
- Params accessed before awaiting
Cause: In Next.js 15+, params is now a Promise that must be awaited
Solution:
export default function Page({ params }: { params: { id: string } }) {
return <div>{params.id}</div>;
}
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <div>{id}</div>;
}
Frequency: High (common with Next.js 15 upgrade)
Issue: Hydration Mismatch
Symptoms:
- Error: "Text content does not match server-rendered HTML"
- UI briefly shows incorrect content then corrects itself
Cause: Server and client rendering different content
Common causes:
- Using
Date.now() or random values
- Accessing browser APIs during render
- Conditional rendering based on browser state
Solution:
function Component() {
return <div>{Date.now()}</div>;
}
"use client";
import { useState, useEffect } from "react";
function Component() {
const [time, setTime] = useState<number | null>(null);
useEffect(() => {
setTime(Date.now());
}, []);
return <div>{time ?? "Loading..."}</div>;
}
Frequency: Medium
Validation Checklist
Before considering Next.js implementation complete:
Best Practices
- Default to Server Components - Only use Client Components when necessary
- Colocate Server and Client - Mix Server and Client Components in the same tree
- Fetch on the server - Use async Server Components instead of useEffect
- Use Next.js Link - Client-side navigation for better UX
- Implement loading states - Create loading.tsx for better perceived performance
- Handle errors properly - Use error.tsx for graceful error handling
- Protect routes with middleware - Don't rely on client-side checks alone
- Type everything - Use TypeScript for better DX and fewer bugs
References
- Previous expertise:
.claude/experts/nextjs-expert/expertise.yaml
- Agent integration:
.claude/agents/agent-nextjs.md
- Official docs: https://nextjs.org/docs