Module 5 · Next.js · Drills
Reading is not knowing. Build every one of these yourself — in a real create-next-app project — before you reveal the solution. Effortful recall is the point.
npm run dev and check the route in the browser, then click "Show solution" to compare. If yours works differently but correctly — great, that's fluency. Tick each box as you go; your progress is saved in this browser.
Drill 1 routing
Create a new route at /about that renders an <h1> saying "About DocChat". Which file and folder do you need?
app/about/page.tsx
export default function AboutPage() { return <h1>About DocChat</h1>; }
The folder name is the URL; page.tsx makes it a real route. Visit /about.
Drill 2 layout
Add a layout.tsx for the /about section that wraps the page in a <main> with a shared heading above the page content.
app/about/layout.tsx
export default function AboutLayout({ children, }: { children: React.ReactNode; }) { return ( <main> <p>DocChat</p> {children} </main> ); }
A layout must render {children} — that's where the page slots in. It persists across navigations within the section.
Drill 3 dynamic route
Create a dynamic route /users/[id] that prints "User <id>" using the value from the URL.
app/users/[id]/page.tsx
export default function UserPage({ params, }: { params: { id: string }; }) { return <p>User {params.id}</p>; }
The folder [id] matches any value; it arrives on params.id. Visit /users/42.
Drill 4 server fetch
Write a server component at /health that awaits your FastAPI /health endpoint and renders the returned status. No useEffect.
app/health/page.tsx
export default async function HealthPage() { const res = await fetch(`${process.env.API_URL}/health`, { cache: "no-store", }); const data = await res.json(); return <p>Backend: {data.status}</p>; }
No "use client", so it runs on the server and can be async. The fetch resolves before the HTML ships.
Drill 5 use client
Convert a static greeting into a client component with a button that toggles between "Hi" and "Bye" using useState.
app/components/Greeting.tsx
"use client"; import { useState } from "react"; export default function Greeting() { const [hi, setHi] = useState(true); return ( <button onClick={() => setHi(!hi)}> {hi ? "Hi" : "Bye"} </button> ); }
The first line "use client" is what unlocks useState and onClick. Without it, both would error.
/documents/[id]: a server component that fetches one document from FastAPI and renders its filename and content. This is the screen a user lands on after clicking a document in the list you built in the lesson.
Build · document detail page
Backend GET /documents/<id> returns { "id": 1, "filename": "intro.pdf", "content": "..." }. Fetch it on the server using params.id and render it; throw on a missing document.
app/documents/[id]/page.tsx
type Doc = { id: number; filename: string; content: string }; export default async function DocumentPage({ params, }: { params: { id: string }; }) { const res = await fetch( `${process.env.API_URL}/documents/${params.id}`, { cache: "no-store" } ); if (!res.ok) throw new Error("Document not found"); const doc: Doc = await res.json(); return ( <article> <h1 className="filename">{doc.filename}</h1> <p>{doc.content}</p> </article> ); }
Note the shape: read params.id → interpolate into the server-side fetch → throw on failure so error.tsx catches it → render. Add a loading.tsx in the same folder for a free spinner. This is the same pattern as the list page, scoped to one record.
Click a card to flip it. Say the answer out loud before you flip — that's the rep that builds storage strength.
page.tsx do?layout.tsx for?{children}. Shared shell."use client" do?onClick.[id]; read the value from params.id.NEXT_PUBLIC_?Tick each only if you can do it without looking:
page.tsx and wrap it with a layout.tsx[id] and read params.idawaits a fetch to FastAPI"use client" + useStateNEXT_PUBLIC_) and which stay server-side/documents/[id] detail page fetching one document