The Complete Guide to Next.js App Router (2026 Edition)
February 22, 2026
- nextjs
- react
- app-router
- 2026

Everything you need: file-based routing, layouts, Server vs Client Components, data fetching, and patterns that scale in 2026.
The Next.js App Router is the default way to build React apps in Next.js. It gives you file-based routing, Server Components, streaming, and built-in loading and error UI—all without a separate config file. This guide covers what you need to ship production apps in 2026: structure, patterns, and code you can copy.
Why the App Router?
The old pages/ directory still works, but the App Router is built for the way React and Next.js work today. You get:
- Server Components by default — less JavaScript in the browser, faster loads.
- Layouts that persist — no full re-mount on navigation, smoother UX.
- Streaming — send the shell first, then stream content as it’s ready.
- Built-in loading and error UI — no manual spinners for route transitions.
New projects should use the App Router unless you have a strong reason to stay on Pages.
Project setup
Create a new app with the App Router (default in Next.js 13+):
npx create-next-app@latest my-app --typescript --tailwind --eslint
Your project will have an app/ folder. Everything under app/ is part of the App Router. You need at least:
app/layout.tsx— root layout (required).app/page.tsx— home page at/.
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
File-based routing
Routes come from the folder structure. Only files named page.tsx (or page.js) create a public URL. Other files (layout.tsx, loading.tsx, components) support the page but don’t add routes.
| File | URL |
|---|---|
app/page.tsx | / |
app/about/page.tsx | /about |
app/blog/page.tsx | /blog |
app/blog/[slug]/page.tsx | /blog/hello-world, /blog/any-slug |
app/shop/[...slug]/page.tsx | /shop/a, /shop/a/b/c (catch-all) |
Dynamic segments use square brackets: [slug], [id]. The page receives params (and searchParams for query strings). In Next.js 15+, params and searchParams are Promises in async pages.
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <article>Post: {slug}</article>;
}
Layouts and templates
A layout.tsx wraps a route segment and all its children. It does not re-mount when you navigate between child routes—so headers, sidebars, and shared chrome stay in place.
- Layouts receive
childrenand, in dynamic segments,params. - Nested folders get nested layouts:
app/blog/layout.tsxwraps every page under/blog.
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section className="blog">
<aside>Blog nav</aside>
{children}
</section>
);
}
template.tsx is like layout.tsx but re-mounts on every navigation. Use it when you need to reset state or run effects on each visit (e.g. animated entrances).
Server vs Client Components
By default, every component in the App Router is a Server Component. It runs only on the server: no useState, useEffect, or browser APIs. You can fetch data, read env vars, and keep secrets on the server. The client never receives their code.
When you need interactivity, add "use client" at the top of the file. Only that module (and what it imports) runs in the browser.
// components/Counter.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [n, setN] = useState(0);
return (
<button onClick={() => setN(n + 1)}>Count: {n}</button>
);
}
Rule of thumb: Use Server Components for as much as possible (data fetching, layout, static content). Add Client Components only where you need events, state, or browser APIs. You can pass Server Components as children or props into Client Components; the server renders them and the client receives the result.
Data fetching
In Server Components you can await fetch or a database call at the top level. No useEffect, no loading state for the initial request. Next.js can cache and dedupe requests.
// app/posts/page.tsx
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 }, // revalidate every 60 seconds
});
if (!res.ok) throw new Error("Failed to fetch");
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul>
{posts.map((p: { id: number; title: string }) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
For fully dynamic data (e.g. user-specific), use fetch(..., { cache: 'no-store' }) or call your DB directly. For static or revalidated data, use next: { revalidate: seconds } or next: { tags: ['posts'] } and then revalidateTag('posts') when content changes.
Loading and error UI
Add loading.tsx in a segment to show a fallback while the segment’s page or layout is loading. Add error.tsx to catch errors in that segment and show a custom error UI (with a “Try again” button that calls reset()).
// app/blog/loading.tsx
export default function BlogLoading() {
return <div>Loading posts…</div>;
}
// app/blog/error.tsx
"use client";
export default function BlogError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<p>Something went wrong.</p>
<button onClick={reset}>Try again</button>
</div>
);
}
Route Handlers (API routes)
Use route.ts (or route.js) in the app directory to create API endpoints. Export functions named after HTTP methods: GET, POST, PUT, DELETE, etc.
// app/api/hello/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const name = searchParams.get("name") ?? "World";
return Response.json({ message: \`Hello, \${name}\` });
}
export async function POST(request: Request) {
const body = await request.json();
return Response.json({ received: body });
}
Metadata and SEO
Export a metadata object or generateMetadata function from layout.tsx or page.tsx to set title, description, and Open Graph tags.
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: { title: post.title, description: post.excerpt },
};
}
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPostBySlug(slug);
return <article>{/* ... */}</article>;
}
Middleware
Create middleware.ts at the project root (next to app/) to run code before a request is completed. Use it for auth checks, redirects, rewrites, or setting headers.
// middleware.ts (at project root)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/dashboard")) {
// Check auth, redirect to /login if needed
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
Quick reference
- Route:
app/<segment>/page.tsx→/<segment>. Dynamic:[slug], catch-all:[...slug]. - Layout:
layout.tsxwraps segment + children; does not re-mount on navigation. - Server Component: default; async, fetch at top level; no hooks in that file.
- Client Component:
"use client"at top; use hooks and browser APIs. - Data: In Server Components,
await fetch()or DB; userevalidateorrevalidateTagfor caching. - Loading/error:
loading.tsx,error.tsxin the segment. - API:
app/api/.../route.tswithGET,POST, etc. - SEO:
metadataorgenerateMetadatain layout/page.
Wrap-up
The App Router is built for the way we build React apps in 2026: Server Components, streaming, and file-based conv...