Module 5 · Next.js · Deep Dive

Next.js App Router Core

You have a FastAPI backend. Now you need a frontend that renders on the server, talks to that API, and keeps secrets server-side — exactly what Next.js was built for.

BasicIntermediateBuild

Why this matters The DocChat frontend — the document list, the upload screen, the chat window — is a Next.js app. Interviewers in the UAE ask for Next.js by name because it's the default React framework employers ship. The reassuring part: server rendering and "fetch data, then render HTML" is exactly what you did in PHP. This lesson maps those instincts onto the App Router so you can build DocChat's UI and explain every choice.
In this lesson
  1. Why Next over plain React
  2. The app/ directory
  3. Routes, layouts & navigation
  4. Server vs client components
  5. Fetching data & env vars
  6. Build: the DocChat documents page
  7. Check yourself

1 · Why Next over plain React

Plain React (Create React App, Vite) ships an empty <div> and a bundle of JavaScript. The browser downloads it, runs it, then fetches your data and draws the page. That's slow to first paint, invisible to search engines, and forces every API key into the browser. Next.js fixes all three:

PHP bridge: server components render on the server like PHP did — query, build the markup, send finished HTML — but with React's component model instead of echo.

Start a new project the official way:

# scaffolds an App Router project with TypeScript
npx create-next-app@latest docchat-web
cd docchat-web
npm run dev   # http://localhost:3000

Accept the defaults (TypeScript, App Router, Tailwind if you like). You get a working app in one command.

2 · The app/ directory

Everything lives in app/. The structure of folders and a few special filenames is the routing system — there's no separate route table to maintain.

FileRole
page.tsxMakes a folder a route. app/about/page.tsx/about.
layout.tsxWraps pages below it — shared header, nav, sidebar.
loading.tsxShown automatically while the page is loading.
error.tsxShown automatically if the page throws.
[id]A dynamic folder — matches any value, passed as a param.
app/
  layout.tsx          # root layout — wraps every page
  page.tsx            # the route "/"
  documents/
    page.tsx          # the route "/documents"
    [id]/
      page.tsx        # the route "/documents/123"
PHP bridge: nesting folders to make URLs is like dropping about.php in a directory — but here the folder name is the path and page.tsx is the handler.

3 · Routes, layouts & navigation

A page.tsx default-exports a React component. A nested folder makes a nested route. A layout.tsx wraps every page beneath it and persists across navigations — perfect for a top bar.

app/documents/layout.tsx
export default function DocsLayout({ children }: { children: React.ReactNode }) {
  return (
    <section>
      <h1>Your documents</h1>
      {children}   {/* the page renders here */}
    </section>
  );
}

Navigate with <Link>, not a plain <a>. It does a fast client-side transition and prefetches the target:

import Link from "next/link";

<Link href="/documents">View documents</Link>

Dynamic routes capture part of the URL. A folder named [id] matches any value; the page receives it as a param:

app/documents/[id]/page.tsx
export default async function DocPage({ params }: { params: { id: string } }) {
  return <p>Showing document {params.id}</p>;
}
loading.tsx & error.tsx (free UX) Drop a loading.tsx in a folder and Next shows it automatically while that route's data is fetching — no spinner state to wire up. Drop an error.tsx and any thrown error renders it instead of a white screen.

4 · Server vs client components

This is the concept that trips people up — and the one interviewers probe. In the App Router, every component is a Server Component by default.

Server Component the default

Runs on the server only. It can be async, can await fetch(...) your backend directly, and can read secrets — but it has no useState, useEffect, or onClick, because there's no browser involved.

// no "use client" — this runs on the server
export default async function Page() {
  const res = await fetch("http://localhost:8000/health");
  const data = await res.json();
  return <p>{data.status}</p>;
}

Client Component opt in with "use client"

Add the string "use client" as the very first line of the file. Now it runs in the browser and can use state, effects, and event handlers — everything interactive.

app/components/Counter.tsx
"use client";
import { useState } from "react";

export default function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>Clicked {n}</button>;
}
Interview: "server vs client components?" Say it crisply: "Server Components are the default — they render on the server, can be async and fetch data directly, and keep secrets server-side, but can't use state or event handlers. Client Components opt in with 'use client' and run in the browser for interactivity. The rule of thumb: fetch and render on the server, and push 'use client' down to only the small interactive leaves — a button, a form, a search box."

5 · Fetching data & env vars

Because a server component can be async, fetching is just await fetch(...) — no useEffect, no loading flags. The result is ready before the HTML is sent.

const res = await fetch(`${process.env.API_URL}/documents`, {
  cache: "no-store",   // always fresh; omit to cache
});
const docs = await res.json();

Environment variables split into two worlds, and confusing them is a real security bug:

VariableVisible where
API_URL, DB_PASSWORDServer only. Safe for secrets and tokens.
NEXT_PUBLIC_API_URLInlined into the browser bundle. Never secret.

Rule: anything prefixed NEXT_PUBLIC_ ships to the browser. Your FastAPI base URL used in server components stays a plain API_URL; only put truly public values behind NEXT_PUBLIC_.

PHP bridge: a server-only env var is your old $_ENV['DB_PASS'] in .env — it never left the server. NEXT_PUBLIC_ is the opposite: deliberately printed into the page the browser downloads.

6 · Build it

Your tangible win Build DocChat's /documents route as a server component that fetches the document list from your FastAPI backend and renders it. No spinners, no useEffect — the list is in the HTML on first paint. This is the first real screen of your capstone's UI.

Assuming API_URL=http://localhost:8000 in .env.local and a backend that returns [{ "id": 1, "filename": "intro.pdf" }, ...]:

app/documents/page.tsx
import Link from "next/link";

type Doc = { id: number; filename: string };

export default async function DocumentsPage() {
  const res = await fetch(`${process.env.API_URL}/documents`, {
    cache: "no-store",
  });
  if (!res.ok) throw new Error("Failed to load documents");
  const docs: Doc[] = await res.json();

  return (
    <ul>
      {docs.map((doc) => (
        <li key={doc.id}>
          <Link href={`/documents/${doc.id}`}>
            <span className="filename">{doc.filename}</span>
          </Link>
        </li>
      ))}
    </ul>
  );
}

Read what just happened: the component is async, it awaits your FastAPI endpoint on the server, throws on failure (so error.tsx can catch it), and each filename links to its detail route. Add a loading.tsx beside it and the spinner is free. That's a production-shaped data screen in ~25 lines.

7 · Check yourself

Answer from memory — retrieval is what moves this from "I read it" to "I know it".

Recall quiz

Which file turns a folder into a route?

What kind of component is the default?

What lets a component use state and clicks?

Which env var reaches the user's browser?

How does a server component get its data?

Primary source ⭐ Next.js — Learn: App Router (Dashboard course). The official, 2026-current walkthrough of routing, layouts, server components, and data fetching — the exact concepts above, built into a real app.