| name | nextjs-app-router |
| description | Build modern Next.js 13+ applications using App Router architecture with Server Components, Client Components, and advanced routing patterns. Use when implementing server-first rendering, creating nested layouts, building parallel routes, implementing intercepting routes, using React Server Components, optimizing data fetching with server-side async components, implementing streaming with Suspense, managing client-side interactivity boundaries, or leveraging Next.js 13+ app directory features for performant, SEO-friendly React applications. |
Next.js App Router - Modern React Framework Patterns
When to use this skill
- Building Next.js 13+ applications with App Router
- Implementing Server Components for server-first rendering
- Creating Client Components for interactive UI elements
- Setting up nested layouts that persist across routes
- Implementing parallel routes for complex UI patterns
- Using intercepting routes for modals and overlays
- Optimizing data fetching with async Server Components
- Implementing streaming and progressive rendering with Suspense
- Managing client/server component boundaries effectively
- Building SEO-friendly applications with server-side rendering
- Leveraging server actions for form handling
- Creating file-based routing with app directory structure
When to use this skill
- Building Next.js 13+ applications with App Router, implementing Server Components, or optimizing React Server Components patterns.
- When working on related tasks or features
- During development that requires this expertise
Use when: Building Next.js 13+ applications with App Router, implementing Server Components, or optimizing React Server Components patterns.
Core Concepts
- Server Components by Default - Components are server-rendered unless marked with 'use client'
- File-based Routing -
/appdirectory structure defines routes - Streaming & Suspense - Progressive rendering for better UX
- Data Fetching - Server-side with automatic caching
- Layouts & Templates - Shared UI that persists across routes
App Router Structure
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page (/)
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 UI
├── dashboard/
│ ├── layout.tsx # Dashboard layout
│ ├── page.tsx # /dashboard
│ └── settings/
│ └── page.tsx # /dashboard/settings
└── api/
└── users/
└── route.ts # API endpoint /api/users
Server vs Client Components
// ✅ Server Component (default - no 'use client')
// app/page.tsx
import { db } from '@/lib/db';
export default async function Page() {
// Fetch data on server
const users = await db.user.findMany();
return (
<div>
<h1>Users</h1>
{users.map(user => <UserCard key={user.id} user={user} />)}
</div>
);
}
// ✅ Client Component (interactive features)
// app/components/Counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
// ✅ Composition: Server Component with Client Component children
// app/page.tsx
import { Counter } from './components/Counter';
export default async function Page() {
const data = await fetchData(); // Server-side
return (
<div>
<h1>Server Data: {data}</h1>
<Counter /> {/* Client interactive component */}
</div>
);
}
Data Fetching
// ✅ Fetch with caching (default: 'force-cache')
async function getUsers() {
const res = await fetch('https://api.example.com/users', {
cache: 'force-cache' // Cached until revalidated
});
return res.json();
}
// ✅ Revalidate every 60 seconds
async function getUsers() {
const res = await fetch('https://api.example.com/users', {
next: { revalidate: 60 }
});
return res.json();
}
// ✅ No caching (always fresh)
async function getUsers() {
const res = await fetch('https://api.example.com/users', {
cache: 'no-store'
});
return res.json();
}
// ✅ Database query (Prisma)
import { db } from '@/lib/db';
async function getUser(id: string) {
return await db.user.findUnique({ where: { id } });
}
Dynamic Routes
// app/posts/[id]/page.tsx
interface PageProps {
params: { id: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export default async function PostPage({ params }: PageProps) {
const post = await db.post.findUnique({ where: { id: params.id } });
return <div>{post.title}</div>;
}
// Generate static paths at build time
export async function generateStaticParams() {
const posts = await db.post.findMany();
return posts.map((post) => ({ id: post.id }));
}
// Generate metadata
export async function generateMetadata({ params }: PageProps) {
const post = await db.post.findUnique({ where: { id: params.id } });
return {
title: post.title,
description: post.excerpt
};
}
Layouts
// app/layout.tsx (Root Layout - Required)
export default function RootLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
// app/dashboard/layout.tsx (Nested Layout)
export default function DashboardLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<Sidebar />
<div className="content">{children}</div>
</div>
);
}
Loading & Error States
// app/dashboard/loading.tsx
export default function Loading() {
return <Spinner />;
}
// app/dashboard/error.tsx
'use client';
export default function Error({
error,
reset
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
Streaming with Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
{/* This loads immediately */}
<QuickStats />
{/* This streams in when ready */}
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
</div>
);
}
async function SlowComponent() {
const data = await slowDatabaseQuery();
return <div>{data}</div>;
}
Route Handlers (API Routes)
// app/api/users/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') || '1';
const users = await db.user.findMany({
skip: (parseInt(page) - 1) * 20,
take: 20
});
return NextResponse.json({ users });
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.user.create({
data: body
});
return NextResponse.json(user, { status: 201 });
}
// Dynamic route: app/api/users/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const user = await db.user.findUnique({ where: { id: params.id } });
if (!user) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(user);
}
Best Practices
- Minimize Client Components - Use 'use client' only when needed (interactivity, hooks)
- Fetch Data on Server - Reduces client bundle, improves security
- Streaming - Use Suspense for progressive rendering
- Metadata - Generate SEO-friendly metadata per route
- Parallel Routes - Use
@folderconvention for parallel rendering - Route Groups - Use
(folder)for organization without affecting URL
Resources
Remember: App Router is server-first. Embrace Server Components, use Client Components sparingly, and leverage streaming for better UX.