Agent skill
Next.js App Router Development
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.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/nextjs-development
SKILL.md
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
// Server Component (default)
async function ServerPage() {
const data = await fetchData(); // Can be async
return <div>{data}</div>;
}
// Client Component
"use client";
import { useState } from "react";
function ClientComponent() {
const [count, setCount] = useState(0); // Can use hooks
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:
// ✅ Correct - async Server Component
async function UserProfile({ userId }: { userId: string }) {
const user = await fetchUser(userId);
return <div>{user.name}</div>;
}
// ❌ Wrong - using useEffect in Server Component
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser); // This will error
}, [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:
// Server Component (page.tsx)
async function Page() {
const data = await fetchData(); // Fetch on server
return <ClientComponent data={data} />; // Pass via props
}
// Client Component
"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 navigationusePathname- current pathuseSearchParams- URL query params
Awaiting Params in Next.js 15+ (CRITICAL)
In Next.js 15+, dynamic route params and searchParams MUST be awaited:
// ❌ Wrong - synchronous params (Next.js 14 pattern)
export default function Page({ params }: { params: { id: string } }) {
return <div>Post {params.id}</div>;
}
// ✅ Correct - async params (Next.js 15+)
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 pagelayout.tsx- Shared layoutloading.tsx- Loading UIerror.tsx- Error UInot-found.tsx- 404 UIroute.ts- API endpoint
Workflows
Workflow 1: Create New Page with Data Fetching
Step 1: Create Page File
// app/posts/page.tsx
import { api } from "@/convex/_generated/api";
import { fetchQuery } from "convex/nextjs";
export default async function PostsPage() {
// Fetch data on server
const posts = await fetchQuery(api.posts.list);
return (
<div>
<h1>Posts</h1>
<PostList posts={posts} />
</div>
);
}
Step 2: Create Client Component for Interactivity
// app/posts/PostList.tsx
"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
// app/posts/loading.tsx
export default function Loading() {
return <div>Loading posts...</div>;
}
Step 4: Add Error Handling
// app/posts/error.tsx
"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
// app/posts/[id]/page.tsx
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 }>;
}) {
// CRITICAL: Await params in Next.js 15+
const { id } = await params;
// Fetch post data
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:
// app/posts/[id]/page.tsx
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
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
try {
// Get query params
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("query");
// Fetch data
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 {
// Parse body
const body = await request.json();
// Validate
if (!body.name || !body.email) {
return NextResponse.json(
{ error: "Name and email required" },
{ status: 400 }
);
}
// Create user
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
// In Client Component
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
// middleware.ts
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
// app/dashboard/page.tsx
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
// app/dashboard/UserProfile.tsx
"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"); // Navigate after login
};
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:
// ❌ Wrong - Server Component trying to use hooks
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0); // Error!
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// ✅ Correct - Client Component
"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"):
// ❌ Wrong
import { useRouter } from "next/router";
// ✅ Correct
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:
// ❌ Wrong - synchronous access (Next.js 14)
export default function Page({ params }: { params: { id: string } }) {
return <div>{params.id}</div>;
}
// ✅ Correct - async/await (Next.js 15+)
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:
// ❌ Wrong - causes hydration mismatch
function Component() {
return <div>{Date.now()}</div>; // Different on server vs client
}
// ✅ Correct - use useEffect for client-only values
"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:
- Server Components are used by default (no unnecessary "use client")
- Client Components have "use client" directive where needed
- Data fetching uses async Server Components (not useEffect)
- Navigation hooks only used in Client Components
- Params and searchParams are awaited in Next.js 15+
- Links use next/link for client-side navigation
- API routes have proper error handling
- Protected routes use middleware for authentication
- Loading states implemented with loading.tsx
- Error handling implemented with error.tsx
- Metadata configured for SEO
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
Didn't find tool you were looking for?