Next.js: React with a server and routing
Lesson 6 of 6
6▲ Next.js: React with a server and routing
The big pictureReact alone renders in the browser: an empty page arrives, then JavaScript loads, then it fetches the data, then it fills in. That is slow on phones, and search engines may see an empty page. Next.js renders on the server first and sends ready HTML with the content, and adds routing, data and APIs in one project. The Darb platform itself is built with it.
What the picture shows: The page is built on the server with its data, arrives ready to read and index, and then only the interactive parts come alive in the browser.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
React ships you flat-pack furniture with a manual and you assemble it at home (the browser). Next.js ships an assembled room, and you only switch on the appliances that need power (the buttons and forms). That is why the page shows sooner and search engines can read it. A Next.js project is arranged like a filing cabinet: each folder in it is a page of your site, named in the URL.
App Router: each folder under app/ is a route segment, page.tsx makes it visitable, layout.tsx wraps children and keeps its state across navigation, loading.tsx and error.tsx are automatic Suspense and Error Boundary limits, and [slug] is a dynamic segment. Components are server components by default (React Server Components): they can be async, reach the database directly and ship no code to the browser, with their result streamed as an RSC payload. "use client" marks a boundary after which components are sent to the client and hydrated, and only serialisable values cross it. Caching and revalidation (revalidate, revalidatePath) decide static (SSG/ISR) or dynamic rendering, generateStaticParams prebuilds dynamic routes, and generateMetadata produces head tags. route.ts defines HTTP handlers, and server actions ("use server") are server functions a form calls directly.
Before You Start
⚛️React
You know components, props, useState and useEffect.
📨HTTP
You know the GET and POST verbs and status codes.
🟩Node.js and Express
You know what a server is and how it answers a request.
The Core Idea
1Why render on the server?
In a plain React app (client-side rendering, CSR) the server sends HTML holding only an empty <div id="root"></div>; then the browser downloads JavaScript, runs it, fetches the data and draws. In server-side rendering (SSR) the server runs the components first and sends HTML with all the content, then JavaScript loads to make the buttons work. The example imitates what reaches the browser in each case:
const menu = [{ name: "Latte", price: 14 }, { name: "Mocha", price: 16 }];
const Menu = (items) => `<ul>${items.map((d) => `<li>${d.name} ${d.price} SAR</li>`).join("")}</ul>`;
// client-side rendering: the first response is an empty shell
const csrResponse = `<body><div id="root"></div><script src="/app.js"></script></body>`;
// server-side rendering: the server already ran the component
const ssrResponse = `<body><div id="root">${Menu(menu)}</div><script src="/app.js"></script></body>`;
const words = (html) => html.replace(/<[^>]+>/g, " ").trim().split(/\s+/).filter(Boolean).length;
console.log("CSR: words a search engine sees first:", words(csrResponse));
console.log("SSR: words a search engine sees first:", words(ssrResponse));
console.log(ssrResponse);The result: with SSR the user sees content sooner (especially on a slow phone), the search engine reads the text directly, and link previews on WhatsApp work. Next.js does this automatically.
2Creating the project and its layout
npx create-next-app@latest cafe # accept the defaults: TypeScript, App Router cd cafe npm run dev # open http://localhost:3000
There is no routes config file: the folder is the route. Each folder inside app/ becomes part of the URL, and specially named files inside it have set roles:
app/
layout.tsx -> wraps every page (header, footer); has <html> and <body>
page.tsx -> /
loading.tsx -> shown while a page in this folder loads
error.tsx -> shown if a page in this folder throws
not-found.tsx -> the 404 page
menu/
page.tsx -> /menu
[slug]/
page.tsx -> /menu/latte, /menu/mocha ...
api/
orders/
route.ts -> GET/POST /api/orders
public/
logo.svg -> served as /logo.svg3Layouts and links
layout.tsx wraps every page beneath it with shared parts (the top bar, the menu, the footer), and is not redrawn as you move between its pages, so the bar never flickers. <Link href="/menu"> from next/link replaces a plain <a> for moving within your site: it prefetches the next page and navigates without a full reload.
// app/layout.tsx
import Link from "next/link";
import "./globals.css";
export const metadata = { title: "Darb Café", description: "Coffee roasted every morning" };
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ar" dir="rtl">
<body>
<header>
<Link href="/">☕ Darb</Link>
<nav><Link href="/menu">Menu</Link> <Link href="/about">About</Link></nav>
</header>
<main>{children}</main>
<footer>© 2026</footer>
</body>
</html>
);
}4Server and client components
The biggest idea in modern Next.js: every component is a server component by default. A server component can be async, read from the database or an API directly, and its code never reaches the browser (so secret keys are safe in it and the page is lighter). But it has no state and no events. A component that needs useState, onClick or useEffect starts with the line "use client".
// app/menu/page.tsx (server component: no "use client")
import { AddButton } from "./AddButton";
export default async function MenuPage() {
const res = await fetch("https://api.example.com/menu", { next: { revalidate: 60 } });
const items: { slug: string; name: string; price: number }[] = await res.json();
return (
<ul>
{items.map((i) => (
<li key={i.slug}>{i.name} · {i.price} SAR <AddButton slug={i.slug} /></li>
))}
</ul>
);
}// app/menu/AddButton.tsx
"use client";
import { useState } from "react";
export function AddButton({ slug }: { slug: string }) {
const [added, setAdded] = useState(false);
return <button onClick={() => setAdded(true)}>{added ? "Added ✓" : "Add"}</button>;
}revalidate: 60 means: keep the fetch result and reuse it, refreshing it at most every 60 seconds. So the page is as fast as a static one, and fresh enough.
"use client" at the top of every file "just in case". You lose server rendering and ship more code. Keep the client boundary as small as possible: the button, not the page.5Loading and error pages
Remember the four interface states from the asynchronous lesson. Next.js turns two of them into files: loading.tsx shows at once while the page's data loads, and error.tsx (a client component) shows if the page throws, with a reset function to try again. You do not write if (loading) by hand on every page.
// app/menu/loading.tsx
export default function Loading() {
return <p>⏳ Loading the menu...</p>;
}
// app/menu/error.tsx
"use client";
export default function MenuError({ error, reset }: { error: Error; reset: () => void }) {
return (
<div role="alert">
<p>Could not load the menu.</p>
<button onClick={reset}>Try again</button>
</div>
);
}6Dynamic routes and page metadata
A folder named in square brackets [slug] catches any value in that part of the URL, which arrives in the page as params. If the item is missing, call notFound() for a proper 404. generateMetadata produces each page's title and description from its data, so "Latte | Darb Café" shows in Google results.
7Route handlers and server actions: the server in the same project
Route handler: a route.ts file exporting functions named after verbs (GET, POST) that return a Response, like a small Express route. Handy for a webhook or an API another app consumes. Server action: a function starting with "use server" that runs on the server, passed straight to a form's action prop, so the form sends its data to it with no fetch and no API you write by hand. For big services a separate server (like Darb's) stays clearer.
// app/api/orders/route.ts
export async function POST(request: Request) {
const body = await request.json();
if (!body.item) return Response.json({ error: "item is required" }, { status: 400 });
// save the order ...
return Response.json({ id: 42, ...body }, { status: 201 });
}// app/contact/page.tsx
import { revalidatePath } from "next/cache";
async function sendMessage(formData: FormData) {
"use server"; // runs on the server only
const text = String(formData.get("text") ?? "").trim();
if (!text) return;
// await db.messages.insert({ text }); <- direct database access is safe here
revalidatePath("/contact");
}
export default function Contact() {
return (
<form action={sendMessage}>
<label htmlFor="text">Your message</label>
<textarea id="text" name="text" required />
<button>Send</button>
</form>
);
}8Deployment: from your machine to a real link
npm run build builds the production version and tells you which pages are static (○) and which dynamic (ƒ). To deploy: push the project to GitHub, then connect it to Vercel (the company behind Next.js) or any platform that runs Node, and it deploys on every git push. Or build it a Docker image, as you will learn in the deployment track.
apps/web/src/app/[lang] is a real Next.js project. Trace a lesson page from the l/[slug] folder to the request that fetches its data, and you will see every concept in this lesson in code that is running in front of you right now.Worked example
Problem: a page per drink at /menu/latte that shows a 404 for a missing drink, is prebuilt, and has the drink's name as its Google title. Solution: generateStaticParams builds the pages at build time, generateMetadata produces the title, and notFound() handles missing ones:
// app/menu/[slug]/page.tsx
import { notFound } from "next/navigation";
const DRINKS = {
latte: { name: "Latte", price: 14, about: "Silky milk over a double espresso." },
mocha: { name: "Mocha", price: 16, about: "Espresso, chocolate and milk." },
};
type Props = { params: Promise<{ slug: string }> };
export function generateStaticParams() {
return Object.keys(DRINKS).map((slug) => ({ slug }));
}
export async function generateMetadata({ params }: Props) {
const { slug } = await params;
const drink = DRINKS[slug as keyof typeof DRINKS];
return { title: drink ? `${drink.name} | Darb Café` : "Not found" };
}
export default async function Drink({ params }: Props) {
const { slug } = await params;
const drink = DRINKS[slug as keyof typeof DRINKS];
if (!drink) notFound();
return (
<article>
<h1>{drink.name} · {drink.price} SAR</h1>
<p>{drink.about}</p>
</article>
);
}Myth vs Reality
| Myth | Reality |
|---|---|
| Next.js replaces React. | It is built on React and adds the server, routing and the build. |
| Every Next.js component runs in the browser. | In the App Router the default is server-side; only what follows "use client" reaches the browser. |
| Next.js only runs on Vercel. | It runs on any Node server or in a Docker container, and Darb is an example. |
Where you meet it at work
🛍️Shops and blogs
Fast pages search engines can read matter for sales.
📊Dashboards
Server components read the database directly with no middle API.
🎓The Darb platform
The interface you are reading this lesson in is written in Next.js.
Test your understanding
Which file makes the /about route visitable?
In the App Router the folder is the segment and page.tsx makes it a page.
A component reads the database with a secret key. Where must it live?
Server component code is never shipped to the browser, so the secret does not leak.
When do you need "use client"?
State and events need the browser; the rest stays on the server.
Why is server rendering better for search engines?
The crawler reads the text directly without running JavaScript.
Which file shows automatically while a page in the same folder loads its data?
loading.tsx is an automatic Suspense boundary for its folder's pages.
Quick review cards
Check Your Understanding
Why is server rendering faster on a phone?
Because the content comes in the first response, so the phone does not wait to download and run JavaScript and fetch data before seeing anything.
What is layout.tsx for?
It wraps every page beneath it with shared parts (header, menu) and is not redrawn on navigation.
How do you show a 404 for a dynamic page with no item?
By calling notFound() from next/navigation.
Must you validate a server action's input?
Yes, it is an entry point to the server that can be called with a crafted request outside your form.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs3
Done with this lesson?
Sign in to save your progress.