Next.js Fundamentals: App Router & Components
Start building with the Next.js App Router. Understand file-based routing, the split between Server and Client Components, client-side navigation with Link, static assets, and how environment variables keep secrets safe.
4 sections · ~25 min · 5-question quiz (pass ≥ 70%)
1App Router Basics: Files Become Routes
Next.js 13+ uses the App Router — a app/ directory where folders define URL segments and special files define UI:
app/
layout.tsx → root shell (wraps every page)
page.tsx → route UI for /
about/
page.tsx → /about
blog/
page.tsx → /blog
[slug]/
page.tsx → /blog/my-post (dynamic segment)
Every route needs a page.tsx to be publicly accessible. Optional files:
layout.tsx— shared UI that wraps child routes (persists across navigation).loading.tsx— instant fallback UI while a segment loads.error.tsx— error boundary for that segment.not-found.tsx— custom 404 for the segment.
Layouts nest automatically. The root app/layout.tsx wraps everything — typically holding <html>, <body>, fonts, and global providers. Nested layouts (app/dashboard/layout.tsx) wrap only their subtree.
Pages are React Server Components by default in the App Router. They can fetch data directly without useEffect, and their JavaScript is not shipped to the client unless they import client-only features.
2Server Components vs Client Components
The App Router's biggest mental shift: components render on the server unless you opt into the client.
Server Components (default):
- Run only on the server — zero client bundle cost for their logic.
- Can
awaitdatabase queries and file reads directly in the component body. - Cannot use hooks (
useState,useEffect) or browser APIs.
Client Components — add "use client" at the top of the file:
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
Use Client Components for interactivity, effects, and browser-only APIs. Keep them as leaf nodes as much as possible — wrap a small interactive island inside a Server Component page:
// app/page.tsx — Server Component (no directive)
import { Counter } from "./Counter";
export default async function HomePage() {
const posts = await db.post.findMany();
return (
<main>
<h1>Latest Posts</h1>
<PostList posts={posts} />
<Counter />
</main>
);
}
This pattern minimizes JavaScript sent to the browser while keeping the page fast and SEO-friendly.
3Link, Navigation & Layouts
Use next/link for client-side navigation — faster transitions without full page reloads:
import Link from "next/link";
export function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog/hello-world">Latest post</Link>
</nav>
);
}
Link prefetches routes in the viewport by default (in production), so clicks feel instant. Use prefetch={false} for rarely visited pages.
Programmatic navigation in Client Components uses the useRouter hook from next/navigation:
"use client";
import { useRouter } from "next/navigation";
function LogoutButton() {
const router = useRouter();
return (
<button onClick={() => {
signOut();
router.push("/login");
}}>
Log out
</button>
);
}
Layouts share chrome across routes. A dashboard layout might render a sidebar once while child pages swap:
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar />
<main>{children}</main>
</div>
);
}
When navigating between /dashboard/settings and /dashboard/billing, the layout persists — only children re-render.
4Public Assets & Environment Variables
Static files that don't need processing live in the public/ directory at the project root:
public/
logo.svg
favicon.ico
images/hero.png
Reference them from the site root — /logo.svg, not /public/logo.svg. Next.js serves them as-is with cache-friendly headers.
For images that benefit from optimization, prefer next/image (covered in the advanced track). For simple icons and downloads, public/ is fine.
Environment variables configure your app per environment. Create .env.local (git-ignored) for secrets:
# .env.local
DATABASE_URL="postgresql://..."
NEXT_PUBLIC_APP_URL="http://localhost:3000"
Rules:
- Variables without the
NEXT_PUBLIC_prefix are available only on the server — safe for API keys and database URLs. - Variables with
NEXT_PUBLIC_are inlined into the client bundle. Never put secrets there. - Access via
process.env.VAR_NAMEin Server Components, route handlers, and server actions. - Client Components can only read
NEXT_PUBLIC_variables.
Restart the dev server after changing .env files — values are loaded at build/start time, not on every request.