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