Next.js App Router
In short: Next.js’ routing and rendering system (since version 13), in which pages are rendered on the server by default (“Server Components”) and only explicitly marked parts run in the browser (“Client Components”, "use client").
In more detail: next/dynamic with ssr:false (excluding a component from server rendering entirely) only works inside a Client Component — in a Server Component (such as layout.tsx without "use client") it’s a hard build error, not a warning. npx tsc --noEmit only checks TypeScript types and doesn’t catch such Next.js-specific rules — for that you need a real next build.
Our context: An ssr:false error of exactly this kind once broke the build — since then, a real npm run build runs before every larger batch, not just the TypeScript check.
In Depth
File-based routing via special file names
The App Router organises routes via the folder structure under app/ — every folder corresponds to a URL segment, and a page.tsx file inside it turns the folder into a real, reachable page (folders without page.tsx are purely structural, e.g. for shared layout.tsx files). This is a fundamental difference from the older Pages Router (pages/ directory), where every file was automatically a route and layouts had to be imported manually into every page. Besides page.tsx there’s a whole series of other reserved file names with a fixed meaning: layout.tsx wraps all pages below it and persists during navigation within the same layout (no re-mount), loading.tsx is automatically shown as a Suspense fallback while a Server Component loads, error.tsx catches runtime errors in its subtree (it must itself be a Client Component), and not-found.tsx is rendered when notFound() is called. Folders in square brackets ([slug]) create dynamic route segments; square brackets with three dots ([...slug]) capture any number of path segments at once (“catch-all” routes).
Server Components as the default
Server Components are the default: they run exclusively on the server, have direct access to databases/secrets, but send NO JavaScript code to the browser themselves — the result is already finished HTML. Client Components ("use client" at the top of the file), on the other hand, are additionally sent to the browser as JavaScript and “hydrated” there so that they become interactive (onClick, useState, useEffect only work there). The rule of thumb: leave as much as possible as Server Components and only add "use client" where real interactivity in the browser is needed — every Client Component increases the JavaScript bundle sent to the browser. Important here: the "use client" directive doesn’t just mark a single component, but the entire module boundary — every component imported by a Client Component also becomes part of the client bundle, even if it doesn’t use useState itself.
// app/posts/[slug]/page.tsx — Server Component (default, no "use client")
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await db.query.posts.findFirst({ where: eq(posts.slug, params.slug) });
return <article>{post?.title}</article>;
}A common pattern is combining Server and Client Components by embedding a small, interactive Client Component as a children prop in a Server Component — the Server Component stays server-only even though it “contains” a Client Component:
// app/layout.tsx — Server Component, renders a Client Component as a child
import { ThemeToggle } from "./theme-toggle"; // Client Component
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ThemeToggle />
{children}
</body>
</html>
);
}Data loading and caching
Server Components can use async/await directly to load data — no useEffect and no separate loading state in the client needed, since the waiting happens on the server before any HTML goes to the browser. Next.js extends the native fetch API with its own caching behaviour: results are cached by default and reused across builds unless cache: "no-store" or a revalidate option is explicitly set. This is a common source of errors for beginners coming from classic React without server rendering who expect an implicit “reload on every request”.
Typical pitfalls
An ssr:false error with next/dynamic (see above) is just one of several Next.js-specific rules that tsc doesn’t detect — mixing server-only and client-only APIs (e.g. window in a Server Component) is also only noticed during next build or at runtime. Another classic mistake: props passed from a Server Component to a Client Component must be serialisable (no functions, no class instances), since they effectively have to be “transported” across the server/client boundary.
See also: next-intl, Cache tags and revalidateTag