What you'll learn

By the end of this you'll understand how the App Router works: folders become URLs, most of your code runs on the server, and you only reach for "use client" when something actually needs the browser. You'll also have a working project structure you won't regret at 11pm when something breaks.

This isn't an exhaustive feature dump. It's the minimum you need to ship something real and not do it wrong.

Who this is for

  • You know React components and props but haven't touched Next.js yet
  • You've used the old Pages Router and want to understand what actually changed
  • You're building a blog, a docs site, or any content site that needs decent SEO

You can skip this if you're already shipping App Router projects and your actual problem is edge caching or advanced ISR. We cover caching in a later guide — we'll link it when it's live.

What is the App Router?

The App Router is Next.js's current routing system. Folders inside app/ define your URLs. That's really the whole idea.

  • app/page.tsx/
  • app/about/page.tsx/about
  • app/developers/nextjs/page.tsx/developers/nextjs

It's also server-first. Files in app/ are Server Components by default, which means HTML is built on the server before it reaches the browser. For SEO that matters a lot — Googlebot doesn't have to wait for JavaScript to hydrate before it can read your content.

Plain English version: folders become URLs, and the server does the heavy lifting.

Prerequisites

You'll need:

  • A basic grip on JavaScript and React (components, props, JSX)
  • Node.js 20 or newer
  • A terminal and any code editor (VS Code and Cursor both work fine)

Check your Node version:

node -v
npm -v

If that prints something ancient, grab the latest LTS from nodejs.org and reopen your terminal.

Setup from zero

Step 1 — Scaffold the project

Run this in whatever folder you keep your side projects:

npx create-next-app@latest my-app

When it asks questions, the answers that matter for this tutorial:

  • TypeScript → Yes
  • ESLint → Yes
  • Tailwind CSS → your choice (yes if you want utility classes)
  • src/ directory → Yes
  • App Router → Yes (this whole guide assumes it)
  • Import alias @/* → Yes

Step 2 — Start dev

cd my-app
npm run dev

Go to http://localhost:3000. You should see the starter page. If the port was occupied, Next.js will tell you which one it used instead.

Step 3 — Know the important folders

Here's what a tidy project looks like after you clean up the starter noise:

src/
  app/
    layout.tsx
    page.tsx
    globals.css
  components/
  modules/

Little tip: routes go in app/, reusable UI goes in components/, and data/business logic goes in modules/ (or lib/ if you prefer). Don't dump everything into app/ just because it technically works.

Core idea / mental model

Three layers. Seriously, just three:

  1. Layout — the chrome that wraps many pages: header, footer, nav
  2. Page — the unique content for one URL
  3. Components — building blocks you reuse

Server Components are the default. They render HTML on the server and ship it to the browser. No client-side JavaScript unless you explicitly ask for it.

When you need browser features — useState, onClick, localStorage — you add "use client" at the top of that file. Not the whole page. Just the component that needs the browser.

Mental model: server by default, client only when you actually need the browser.

And if you internalize just that one sentence, the rest of the App Router starts making sense pretty quickly.

Key terms

  • App Router — folder-based routing under app/
  • Server Component — renders on the server; ships no client JS by default
  • Client Component — runs in the browser; marked with "use client"
  • Layout — shared wrapper that persists across child routes
  • Metadata — title/description for SEO and social cards
  • Route segment — one folder level in your URL path
  • Dynamic segment — a folder like [slug] that matches any value

Step-by-step: building real routes

Step 1 — Fix the home page

Open src/app/page.tsx and replace the starter:

export default function HomePage() {
  return (
    <main>
      <h1>Baseline Notes</h1>
      <p>Practical Next.js guides for developers who want to ship things.</p>
    </main>
  );
}

No "use client". No hooks. Just a function that returns JSX.

Step 2 — Add an About page

Create the folder src/app/about/ and put page.tsx in it:

export default function AboutPage() {
  return (
    <main>
      <h1>About</h1>
      <p>We write Next.js tutorials that don't make you want to close the tab.</p>
    </main>
  );
}

Visit /about. The folder name became the URL. That's the whole routing system, mostly.

Step 3 — Nested route

Create src/app/developers/nextjs/page.tsx:

export default function NextjsHubPage() {
  return (
    <main>
      <h1>Next.js notes</h1>
      <p>Two folders deep. Two URL segments. It just works.</p>
    </main>
  );
}

Visit /developers/nextjs.

Step 4 — Root layout (the shared chrome)

Edit src/app/layout.tsx:

import type { ReactNode } from "react";
import "./globals.css";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <header>
          <a href="/">Home</a>
          {" · "}
          <a href="/about">About</a>
        </header>
        {children}
        <footer>© Baseline Notes</footer>
      </body>
    </html>
  );
}

The header and footer stay on every page. Only {children} changes.

Little tip: swap raw <a> tags for Next.js Link for internal navigation — it prefetches and feels snappier:

import Link from "next/link";

<Link href="/about">About</Link>

Working examples

Example A — dynamic blog route

Create src/app/blog/[slug]/page.tsx:

type Props = { params: Promise<{ slug: string }> };

export default async function BlogPostPage({ params }: Props) {
  const { slug } = await params;
  return (
    <main>
      <h1>Post: {slug}</h1>
      <p>One file. Every URL. Try visiting /blog/hello-world.</p>
    </main>
  );
}

The square brackets mean "this segment can be anything." One file serves every blog post URL.

Example B — async Server Component with data

async function getPosts() {
  return [
    { slug: "app-router", title: "App Router basics" },
    { slug: "images-fonts", title: "Images and fonts" },
  ];
}

export default async function BlogIndexPage() {
  const posts = await getPosts();
  return (
    <main>
      <h1>Blog</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.slug}>{post.title}</li>
        ))}
      </ul>
    </main>
  );
}

The page is async. That's expected and fine for Server Components. In a real project, swap getPosts for a database call or module import.

Example C — Client Component island

This is the pattern worth internalizing. Keep the page server-side, extract only the interactive piece:

src/components/LikeButton.tsx:

"use client";

import { useState } from "react";

export function LikeButton() {
  const [likes, setLikes] = useState(0);
  return (
    <button type="button" onClick={() => setLikes((n) => n + 1)}>
      Likes: {likes}
    </button>
  );
}

Use it inside a server page:

import { LikeButton } from "@/components/LikeButton";

export default function PostPage() {
  return (
    <main>
      <h1>My post</h1>
      <LikeButton />
    </main>
  );
}

The server renders everything. The browser only hydrates the tiny button. Don't fight this pattern — it's the whole point.

Metadata for SEO

Export a metadata object from any page or layout:

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Next.js App Router basics — Baseline Notes",
  description: "Learn App Router routing, layouts, and Server Components from zero.",
};

For dynamic titles (like blog posts that pull from a database), you'd use generateMetadata instead. That's a whole separate guide coming soon.

Patterns and options to know

A few common patterns that come up early:

  • Thin route file + separate data modulepage.tsx calls a function from modules/content/; keeps routes clean
  • Nested layout — create a layout.tsx inside a sub-folder for section-specific chrome (a sidebar, a sub-nav)
  • Loading state — create loading.tsx next to page.tsx; Next.js shows it while the async page resolves
  • Not found — create not-found.tsx to customize the 404 experience per route segment

When to use "use client": forms with local state, toggle components, anything that reads from window or localStorage. Not for entire pages.

src/
  app/                   # routes only, thin files
    layout.tsx
    page.tsx
    blog/
      [slug]/
        page.tsx
    developers/
      nextjs/
        page.tsx
  components/            # shared UI
  modules/
    content/             # posts, types, data helpers
    seo/                 # metadata, JSON-LD

Routes stay thin. Logic stays testable. You won't confuse "URL files" with "data code."

Common mistakes

  • Putting "use client" on the whole page because only a button needs it
  • Fetching SEO-critical data in the browser instead of the server
  • Mixing Pages Router (pages/) patterns into App Router projects without thinking
  • Forgetting lang="en" on <html> (accessibility and SEO both care, takes three seconds to fix)
  • Using raw <a href> for every internal link when Link is right there
  • Only running npm run build right before a deploy — production catches things dev hides

Troubleshooting

  • 404 on a new page — folder might have a typo, or the file is Page.tsx not page.tsx (case-sensitive on Linux/Vercel)
  • useState error / onClick does nothing — missing "use client" at the top of that component
  • Styles completely missingglobals.css needs to be imported in layout.tsx
  • Port 3000 occupied — Next.js prints the alternative port; or kill the old process
  • @/ import not resolving — check paths config in tsconfig.json

Checklist before you ship

  • [ ] Project runs with npm run dev without errors
  • [ ] Home + at least one nested route both render correctly
  • [ ] Root layout has lang="en" and working navigation
  • [ ] Pages are Server Components by default (no "use client" unless actually needed)
  • [ ] Client JS only on interactive islands, not whole pages
  • [ ] Basic metadata set at minimum on root layout
  • [ ] Internal navigation uses Link not raw <a>
  • [ ] npm run build completes without errors

Practice task

Build this in around 20–30 minutes:

  1. Home page with a short intro
  2. /about page with a sentence or two
  3. /blog list page — 3 hardcoded posts
  4. /blog/[slug] page that shows the slug in the h1
  5. Root layout with nav links to /, /about, and /blog

If that works end-to-end, you've got the App Router fundamentals down.

FAQ

Do I still need the Pages Router?
Not for new projects. It still works in existing codebases, but App Router is the current approach and where all Next.js development is going.

Isn't Server Components complicated?
The rule isn't that bad: no hooks, no event handlers in Server Components. When you need those things, push them into a small "use client" child component.

Can I use React Query or SWR?
Yes, inside Client Components. For public blog-type pages where data doesn't change per user, server rendering is usually cleaner and better for SEO anyway.

Is the src/ directory required?
Nope. app/ at the root works fine. src/ is just a tidiness preference.

What about API routes?
Route Handlers in app/api/.../route.ts. That's a separate guide.

What to learn next

  1. Next.js images and fonts — the companion Phase 1 post; covers next/image, LCP, and next/font
  2. Metadata and SEOgenerateMetadata, JSON-LD, Open Graph
  3. Caching and revalidate — when and how Next.js updates content
  4. Route Handlers — simple APIs without a separate server
  • Next.js images and fonts — the other Phase 1 guide, good follow-up read
  • Next.js metadata and SEO — coming soon
  • Next.js caching and revalidate — coming soon
  • Baseline tools: JSON Formatter, JWT Decoder — free, no login
  • Developers hub — React, MERN, Node.js notes

Takeaways

  • Folders in app/ become URLs. That's the routing system.
  • Server Components are the default — great for SEO, ships less JS
  • Layouts share persistent chrome; pages are the unique parts
  • "use client" is for interactive islands, not entire pages
  • Keep routes thin; put logic in modules

If you remember only one thing: server by default, client only when you need the browser.