Next.js App Router Conventions
Server Components by Default
- Use Server Components for all pages and layouts — they run on the server with zero client JS
- Add
'use client' only when the component needs interactivity (event handlers, hooks, browser APIs)
- Keep client components small and push them to the leaves of the component tree — minimizes the client JS bundle
- Pass server data to client components via props — do not fetch on the client what you can fetch on the server
// app/products/page.tsx — Server Component (default)
import { getProducts } from '@/lib/data';
import { AddToCartButton } from './add-to-cart-button';
export const metadata = {
title: 'Products',
description: 'Browse our product catalog',
};
export default async function ProductsPage() {
const products = await getProducts();
return (
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name} — <AddToCartButton productId={p.id} />
</li>
))}
</ul>
);
}
Caching (Next.js 15+)
- Fetch requests are NOT cached by default — explicitly opt in with
next: { revalidate: N } or next: { tags: ['key'] }
- Use
"use cache" directive (Next.js 16+) for explicit caching of pages, components, and functions
- GET route handlers are uncached by default — use
export const dynamic = 'force-static' to opt into caching
- Use
revalidateTag() and revalidatePath() for on-demand cache invalidation after mutations
Partial Prerendering (PPR)
- Enable PPR via
ppr: 'incremental' in next.config.ts to combine static shells with dynamic streamed content
- Place
<Suspense> boundaries close to dynamic components — everything outside the boundary prerenders as static
- PPR eliminates the binary choice between static and dynamic — use for pages with both cached and personalized content
Turbopack
- Use Turbopack for development (stable since Next.js 15) — 2-5x faster compilation than Webpack
- Use
next build --turbopack (beta in 15.5+) for faster production builds
Server Actions
- Use Server Actions for form mutations — define with
"use server" directive, call directly from <form action>
- Validate all Server Action inputs server-side — they are public HTTP endpoints despite inline syntax
- Use
next/after to run code after response streaming completes — for analytics, logging, and cache warming
Parallel & Intercepting Routes
- Use
@slot folders for parallel routes — render multiple pages in the same layout simultaneously (dashboards, modals)
- Use intercepting routes
(..) to show route content in a modal while preserving the URL for deep linking
Route Handlers
- Place API endpoints in
app/api/.../route.ts files
- Export named functions matching HTTP methods:
GET, POST, PUT, DELETE
- Return
NextResponse.json() with appropriate status codes
- Validate request bodies before processing
Images & Fonts
- Use
next/image for all images — it handles lazy loading, resizing, and format conversion
- Use
next/font to load fonts — eliminates layout shift and external network requests
- Set explicit
width and height or use fill to prevent cumulative layout shift
Middleware
- Use
middleware.ts at the project root for auth checks, redirects, and geolocation logic
- Keep middleware fast — it runs on every matched request at the edge; slow middleware adds latency to every page load
- Use
matcher config to limit which routes trigger the middleware
Error & Loading Boundaries
- Add
loading.tsx in route segments for instant loading states with Suspense
- Add
error.tsx (client component) in route segments to catch and display errors gracefully
- Add
not-found.tsx for custom 404 pages per route segment
- Use
global-error.tsx to catch errors in the root layout
SEO
- Export a
metadata object or generateMetadata() function from every page
- Include
title, description, and openGraph properties at minimum
- Use
generateStaticParams() for dynamic routes to enable static generation — pre-rendered pages load instantly
- Add
robots.ts and sitemap.ts files at the app root