App Router has a lot of new vocabulary — server components, client components, layouts, route handlers, parallel routes, intercepting routes, server actions. For an indie content site, eight concepts cover 95% of what you will touch. Learn these, ignore the rest until you need them.
Background
App Router landed in Next.js 13 and matured through 14 and 15. It is the recommended default in 2026. The Pages Router still works, but new features (PPR, server actions, improved caching) all land here first. The mental model is different enough from Pages Router that “I know React” alone is not sufficient.
How to tell
- Your code throws “You’re importing a component that needs
useState. It only works in a Client Component” — you need the server/client boundary. - A layout never re-renders when you navigate between pages — that is the point, learn shared layouts.
fetch()calls inside a server component are being cached without you asking — caching defaults matter.- A
loading.tsxfile mysteriously controls suspense — file conventions are a real concept here.
Step by step
- Server Components by default. Every file in
app/is a server component unless you write'use client'at the top. They canawaitdirectly, ship zero JS to the browser:
// app/page.tsx — server component
import { db } from '@/lib/db';
export default async function HomePage() {
const posts = await db.post.findMany({ take: 10 });
return (
<ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
);
}
- Client Components.
'use client'opts the file (and its imports) into the client bundle. Needed foruseState,useEffect, event handlers:
'use client';
import { useState } from 'react';
export function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>Count: {n}</button>;
}
You can import a client component into a server component freely. The reverse needs children:
// app/page.tsx (server) — passes a server-rendered child to a client wrapper
import { ClientShell } from './ClientShell';
import { ServerData } from './ServerData';
export default function Page() {
return <ClientShell><ServerData /></ClientShell>;
}
- File-system routes. The folder structure literally is the URL:
app/
├── layout.tsx → wraps every page
├── page.tsx → /
├── blog/
│ ├── layout.tsx → wraps /blog/*
│ ├── page.tsx → /blog
│ └── [slug]/
│ ├── page.tsx → /blog/:slug
│ ├── loading.tsx → suspense boundary
│ └── not-found.tsx → 404 inside this segment
└── (marketing)/ → route group, not in URL
├── about/page.tsx → /about
└── pricing/page.tsx → /pricing
- Layouts. They wrap their subtree and do NOT re-render on navigation within that subtree — that’s the point:
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<nav>...</nav> {/* persists across all routes */}
{children}
</body>
</html>
);
}
- Static / Dynamic / ISR. Routes are static by default. Touching
cookies(),headers(), orsearchParamsmakes them dynamic. ISR viaexport const revalidate:
// app/articles/[slug]/page.tsx
export const revalidate = 3600; // ISR: regenerate at most every hour
export async function generateStaticParams() {
const articles = await getAllArticles();
return articles.map(a => ({ slug: a.slug }));
}
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const article = await getArticle(params.slug);
return <article><h1>{article.title}</h1></article>;
}
- Data fetching with extended
fetch. Caching is opt-in via options — defaults changed in Next 15:
// no caching, fresh every request
const data = await fetch(url, { cache: 'no-store' });
// revalidate every 60 seconds
const data = await fetch(url, { next: { revalidate: 60 } });
// tag-based revalidation — call revalidateTag('posts') later
const data = await fetch(url, { next: { tags: ['posts'] } });
- Metadata API. Set
<title>, OG, canonical via a typedmetadataexport.generateMetadatafor dynamic per-route metadata:
// app/articles/[slug]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const article = await getArticle(params.slug);
return {
title: article.title,
description: article.description,
alternates: { canonical: `/articles/${article.slug}/` },
openGraph: { title: article.title, url: `/articles/${article.slug}/` },
};
}
- Server Actions.
'use server'functions are callable from client components but run server-side. Replaces a lot of/api/*route boilerplate:
// app/actions.ts
'use server';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';
export async function createPost(formData: FormData) {
const title = formData.get('title')?.toString() ?? '';
await db.post.create({ data: { title } });
revalidateTag('posts');
}
// app/new/page.tsx — used in a form
import { createPost } from '../actions';
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Save</button>
</form>
);
}
Common pitfalls
- Putting
'use client'at the top of every file “to be safe” — you have just rebuilt a SPA and lost the framework benefits. - Importing a server-only library inside a client component — you will get a build error or, worse, leak secrets.
- Forgetting that
layout.tsxdoes NOT re-render on navigation within its scope — putting per-page state there will surprise you. - Mixing
searchParams(which makes a route dynamic) into a page you wanted static — checkdynamicexports. - Treating server actions as RPC for any heavy work — they are HTTP under the hood, with the same cold-start cost.
Who this is for
Anyone starting a new Next.js project in 2026 or migrating from Pages Router and wondering what changed.
When to skip this
Existing Pages Router projects with no migration appetite — App Router can run alongside, but full conversion is real work.
FAQ
- Do I need to learn React Server Components separately?: RSC is the foundation of App Router — learning App Router is learning RSC. You do not need a separate RSC tutorial.
- Can I still use
getStaticProps?: Only in the Pages Router. App Router replaces it withasyncserver components andfetchoptions. - What about
getServerSideProps?: Equivalent iscache: 'no-store'onfetch, or callingcookies()/headers()to force dynamic. - Are server actions production-ready?: Yes, stable since Next.js 14. Fine for forms and mutations. Still review the security model — they are public endpoints.