Next.js App Router in Production · Rendering, Caching and Mutations
Server Components, Client Components and the Boundary
Render data on the server with Server Components, add interactivity with small Client Components, and pass only serializable props across the boundary.
What you'll build
A shop page whose product list is a Server Component reading from a server-only data module, with an interactive Client Component (an "Add to cart" button with state) for each product. Every example in this course is a real Next.js 16 project: the files are built with next build, started with next start, and the requests shown are sent to it.
The idea
- In the App Router, components are Server Components by default: they run on the server (at build time or per request), can be
async, read databases and secrets, and send no JavaScript for themselves to the browser. "use client"marks a module as a Client Component entry point: it's rendered to HTML on the server too, then hydrated in the browser, where state, effects and event handlers work.- Props passed from server to client components must be serializable, and everything passed is visible in the page's payload. Keep client components small and at the leaves.
import "server-only"makes the build fail if a module with secrets or database access is imported into client code.- These examples enable Cache Components (
cacheComponents: true), the default for new apps in Next.js 16; pages are prerendered into a static shell where possible.
The code
// file: next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = { cacheComponents: true, partialPrefetching: true };
export default nextConfig;
// file: app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body><main>{children}</main></body>
</html>
);
}
// file: app/lib/products.ts
import "server-only";
import { cacheLife } from "next/cache";
export type Product = { id: string; name: string; price: number };
export async function getProducts(): Promise<Product[]> {
"use cache";
cacheLife("hours");
// A real app queries its database here, with credentials only the server has.
return [
{ id: "p1", name: "Mug", price: 8.5 },
{ id: "p2", name: "Teapot", price: 24 },
];
}
// file: app/add-to-cart.tsx
"use client";
import { useState } from "react";
export function AddToCart({ productId }: { productId: string }) {
const [count, setCount] = useState(0);
return (
<button data-product={productId} onClick={() => setCount(count + 1)}>
Add to cart ({count})
</button>
);
}
// file: app/page.tsx
import { getProducts } from "./lib/products";
import { AddToCart } from "./add-to-cart";
export default async function Page() {
const products = await getProducts(); // runs on the server; no data-fetching code ships to the browser
return (
<>
<h1>Shop</h1>
<ul>
{products.map((p) => (
<li key={p.id}>
<span>{p.name}: ${p.price.toFixed(2)}</span>{" "}
<AddToCart productId={p.id} />
</li>
))}
</ul>
</>
);
}
// headers: x-nextjs-cache
// request: GET /Output
GET / -> 200 (x-nextjs-cache: HIT) Shop Mug: $8.50 Add to cart (0) Teapot: $24.00 Add to cart (0)
Run this on your computer — see “Get Started” for setup.
The client component's initial HTML ("Add to cart (0)") is rendered on the server; clicks update it in the browser after hydration. The x-nextjs-cache: HIT header shows the page was prerendered at build time.
Common mistakes
- Adding
"use client"at the top of pages or layouts, turning whole trees into client code and shipping data-fetching logic to the browser. - Passing whole database records to client components; every prop is serialized into the page.
- Fetching in
useEffectfrom client components what a Server Component could read directly. - Importing a server-only module (database, secrets) from a client component; protect it with
import "server-only".
Key points
- Server Components by default: async, data access, no client JavaScript.
- Client Components for interactivity, kept small and at the leaves.
- Only serializable, non-secret props cross the boundary.
Further reading: Server and Client Components · Cache Components
Practice questions
What does "use client" do?
Answer
It marks a module as the entry to client code: that component and its imports are bundled for the browser, rendered to HTML on the server, then hydrated so state and event handlers work.
Why keep Client Components small and at the leaves?
Answer
Everything under a client boundary ships JavaScript and can't directly read server data, so small islands keep bundles small and data fetching on the server.
What does import "server-only" protect against?
Answer
It makes the build fail if the module is imported from client code, preventing database code or secrets from being bundled for the browser.
Open in the app to mark lessons complete, take notes and track your progress.