Skip to main content
CodeOath
← All posts

Next.js75 min total · 12 parts

Next.js Fundamentals: The App Router, Server Components, and the Caching Rules That Just Flipped

Part 1 of 12 · ~3 min

Overview

Ask React what it's responsible for and the answer is narrow on purpose: given some state, figure out what should be on screen. It has nothing to say about which URL produces which screen, whether the first bytes a visitor's browser receives already contain real content, or how an app with forty routes avoids handing every visitor the JavaScript for all forty of them. Next.js exists to answer those three questions, and the thing that actually slows people down isn't the API surface — it's a mental adjustment: your code runs on a server by default now, there's an opinionated cache sitting between you and your data, and one directive ("use client") draws a line through a tree of components instead of flipping a switch on a single file.

We'll feel all three of those at once by building one real thing from start to finish: OpenRoles, a small job board. A candidate lands on the homepage, browses open roles, opens one, applies. An employer signs in, posts a role, watches applications land, and eventually pays to bump a listing to the top. None of that is glamorous — a list, a detail page, a locked-down dashboard, a form, a payment webhook — which is exactly why it's useful here: it's the shape of an enormous number of real products, so getting it right pulls in every mechanism in this reference for an actual reason, roughly in the order you'd hit it yourself.

Here's OpenRoles before Next.js shows up at all — a plain client-rendered React app:

function App() {
  const [jobs, setJobs] = useState([]);

  useEffect(() => {
    fetch("/api/jobs")
      .then((res) => res.json())
      .then(setJobs);
  }, []);

  return (
    <ul>
      {jobs.map((job) => (
        <li key={job.id}>{job.title} — {job.company}</li>
      ))}
    </ul>
  );
}

This runs, but think about what a visitor's browser actually receives first: an empty <div id="root">, then a JavaScript bundle, then — once that bundle parses and fires its fetch — finally, some job titles. Someone on a train with a spotty connection stares at a blank tab for a beat. Worse for a job board specifically: a search crawler that doesn't sit around executing scripts sees that same empty <div> and nothing else, on a site whose entire business model depends on candidates finding a listing through search. And every feature OpenRoles bolts on afterward — an analytics chart for the employer dashboard, a rich-text editor for job descriptions — rides along in that one bundle to every visitor, including the anonymous candidate who reads three titles and closes the tab.

By the time this reference is done, the same app will send real HTML for its listings before any script has run, keep the employer's dashboard private and always current, take an application from a form that still functions even if the JavaScript never loads, and hold back its heavier employer-only tooling until an employer actually signs in. Each chapter below closes one specific gap between those two versions of the app, and whatever Next.js feature closes that gap is the chapter's subject. Read straight through for the whole story, or skip to whatever you're stuck on — every chapter opens by naming which version of OpenRoles it's picking up.

One shape worth keeping in mind throughout — this is a job record, exactly as it flows through the rest of the app:

type Job = {
  id: string;
  slug: string;
  title: string;
  company: string;
  companyLogoUrl: string;
  location: string;       // "Remote" | a city
  salaryMin: number;
  salaryMax: number;
  featured: boolean;      // true once an employer pays to promote it
  postedAt: string;       // ISO timestamp
  applicantCount: number;
};