Next.js App Router patterns including server components, data fetching, routing, layouts, and caching strategies. Use this skill when building or maintaining Next.js 13+ applications with the App Router. Covers server vs client components, streaming, metadata, and deployment patterns.
Components in the App Router are Server Components by default. Only add 'use client' when the component needs interactivity, browser APIs, or React hooks that depend on client state.
Keep as Server Components:
Mark as Client Components ('use client'):
Push 'use client' as far down the tree as possible. Wrap only the interactive leaf, not the entire page.
// app/dashboard/page.tsx (Server Component)
import { InteractiveFilter } from './InteractiveFilter';
export default async function DashboardPage() {
const data = await fetchDashboardData();
return (
<div>
<h1>Dashboard</h1>
<InteractiveFilter /> {/* Only this is a Client Component */}
<StaticDataTable data={data} />
</div>
);
}
Fetch data directly in Server Components using async/await. Do not use useEffect for data fetching in server-rendered pages.
// app/users/page.tsx
async function getUsers() {
const res = await fetch('https://api.example.com/users', {
next: { revalidate: 60 }, // ISR: revalidate every 60 seconds
});
if (!res.ok) throw new Error('Failed to fetch users');
return res.json() as Promise<User[]>;
}
export default async function UsersPage() {
const users = await getUsers();
return <UserList users={users} />;
}
cache: 'force-cache' (default) for static data.cache: 'no-store' for real-time data that must be fresh on every request.next: { revalidate: N } for time-based ISR.revalidatePath() or revalidateTag() in Server Actions for on-demand revalidation.unstable_cache or React's cache() to deduplicate within a request.layout.tsx wraps child routes and preserves state across navigation. Use for shared UI (nav, sidebar).page.tsx is the unique content for a route segment.loading.tsx provides instant loading UI using React Suspense.error.tsx (must be 'use client') catches errors in a route segment.not-found.tsx handles 404s. Trigger with notFound() from next/navigation.app/
layout.tsx # Root layout (html, body, global providers)
page.tsx # Home page
dashboard/
layout.tsx # Dashboard layout (sidebar)
page.tsx # /dashboard
settings/
page.tsx # /dashboard/settings
(auth)/
login/page.tsx # /login (grouped without affecting URL)
register/page.tsx # /register
(groupName) to organize without affecting the URL.[slug] for dynamic segments and [...slug] for catch-all segments.@modal parallel routes for modal patterns that preserve URL state.Use Server Actions for form submissions and data mutations. Define them with 'use server' at the top of the function or file.
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.create({ data: { title, content } });
revalidatePath('/posts');
}
useFormStatus for pending states and useFormState for returned errors.action or direct invocation.Use the metadata export or generateMetadata for dynamic metadata.
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.coverImage] },
};
}
metadata in app/layout.tsx with defaults (title template, description).title: { template: '%s | MySite', default: 'MySite' } for consistent page titles.Use loading.tsx or manual <Suspense> boundaries to stream content progressively.
import { Suspense } from 'react';
export default function Page() {
return (
<>
<h1>Dashboard</h1>
<Suspense fallback={<ChartSkeleton />}>
<SlowChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<SlowDataTable />
</Suspense>
</>
);
}
Use middleware.ts at the project root for authentication checks, redirects, and header manipulation. Keep middleware fast -- do not run heavy logic or database queries.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('session');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = { matcher: ['/dashboard/:path*'] };
server-only package to enforce boundaries.useRouter from next/router -- use next/navigation in the App Router.'use client' boundaries. Each one creates a new client bundle entry point.