Building Systems with Next.js (App Router)
How modern Next.js splits work between server and browser, plus several version 16 rules that differ from the older tutorials still circulating.
Next.js is a React framework that adds routing, server rendering and a build pipeline. Its biggest departure from plain React: some components run on the server and are never shipped to the browser at all.
Folders are routes
| File | Role |
|---|---|
| app/page.tsx | The page at / |
| app/knowledge/page.tsx | The page at /knowledge |
| app/knowledge/[slug]/page.tsx | A dynamic segment matching /knowledge/anything |
| app/layout.tsx | The frame wrapping the pages beneath it |
| app/loading.tsx | What shows while data is still coming |
| app/not-found.tsx | The 404 view |
| app/api/…/route.ts | An HTTP endpoint rather than a page |
Server Components and Client Components
Every component is a Server Component by default. It runs on the server, may touch a database and secrets, and only its rendered output reaches the browser. The moment a component needs state, an event handler, or a browser API, mark it with 'use client' on the first line.
The part people miss: 'use client' is not a per-component label, it is a boundary. Everything that file imports joins the browser bundle. So push the directive as deep as possible — onto the button or form that is genuinely interactive, not onto the layout that wraps everything.
// app/visitors/[id]/page.tsx — Server Component
export default async function Page(props: PageProps<'/visitors/[id]'>) {
const { id } = await props.params; // params is a Promise since v15
const visitor = await getVisitor(id); // credentials stay on the server
return <CheckInButton visitorId={visitor.id} />;
}
// app/ui/check-in-button.tsx
'use client';
export default function CheckInButton({ visitorId }: { visitorId: number }) {
const [loading, setLoading] = useState(false);
// ...
}Note the awaited params. Since Next.js 15, params and searchParams are Promises. Older code that reads params.id directly will fail on this version — one of the most common errors when following an out-of-date tutorial.
What changed about caching in 16
- fetch is no longer cached by default. It used to be, and plenty of tutorials still teach the old behaviour.
- To keep a result, use the 'use cache' directive on the function or component, paired with cacheLife to set its lifetime.
- The Cache Components model is switched on with cacheComponents: true in next.config.ts.
- Pages whose data never changes are rendered once at build time — like the notes you are reading.
Changing data with a Server Action
Saving data does not require building your own API endpoint. An async function marked 'use server' can be called straight from a form, and Next.js handles the request. The consequence: that function can be POSTed to directly from outside the app, so authentication still has to be checked inside it.