The Complete Next.js App Router Guide | Server Components, Streaming, Parallel Routes
What this post covers
This guide walks through Next.js 13+ App Router with practical examples: Server Components, Streaming, Server Actions, Parallel Routes, and Intercepting Routes—everything you need to apply in production.
From the field: Migrating from the Pages Router to the App Router, we improved initial load time by about 50% and significantly reduced code complexity.
Introduction: “The Pages Router feels too complex”
Real-world scenarios
Scenario 1: getServerSideProps is hard to manage
Data-fetching logic is scattered. The App Router lets you fetch directly inside components.
Scenario 2: Sharing layouts is awkward
_app.tsx becomes a mess. The App Router supports nested layouts.
Scenario 3: Loading state is tedious
You juggle useState. The App Router keeps it simple with Suspense.
1. What is the App Router?
Core characteristics
The App Router is the new routing system in Next.js 13+.
Key benefits:
- Server Components: render on the server
- Streaming: progressive rendering
- Server Actions: simpler form handling
- Nested layouts: shared layout trees
- Parallel Routes: render multiple segments in parallel
2. File-based routing
Basic structure
Below is a detailed layout using a directory tree. Read each part to see how routes map to files.
app/
├── layout.tsx # Root layout
├── page.tsx # / (home)
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── layout.tsx # Blog layout
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
└── dashboard/
├── layout.tsx
├── page.tsx # /dashboard
└── settings/
└── page.tsx # /dashboard/settings
3. Server Components
Basic usage
Below is a TypeScript example. It uses async/await for data loading and maps over the result—follow each piece to see how it fits together.
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store', // Always fetch fresh data
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}
Caching strategies
The following snippets show typical fetch caching options in TypeScript.
// Static generation (default)
fetch('https://api.example.com/posts', {
cache: 'force-cache',
});
// Fetch fresh data on every request
fetch('https://api.example.com/posts', {
cache: 'no-store',
});
// Revalidate every 10 seconds
fetch('https://api.example.com/posts', {
next: { revalidate: 10 },
});
4. Client Components
The 'use client' directive
// app/components/Counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Combining Server and Client
Below is a TypeScript example: a Server Component imports a Client Component, fetches data asynchronously, and renders a list.
// app/posts/page.tsx (Server Component)
import { Counter } from './Counter';
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Posts</h1>
<Counter /> {/* Client Component */}
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
5. Loading & error UI
loading.tsx
// app/posts/loading.tsx
export default function Loading() {
return (
<div>
<p>Loading posts...</p>
</div>
);
}
error.tsx
// app/posts/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
6. Server Actions
Forms
// app/posts/new/page.tsx
import { redirect } from 'next/navigation';
async function createPost(formData: FormData) {
'use server';
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
redirect('/posts');
}
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<button type="submit">Create</button>
</form>
);
}
useFormStatus
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
);
}
7. Parallel Routes
Layout structure
app/
└── dashboard/
├── layout.tsx
├── @analytics/
│ └── page.tsx
├── @notifications/
│ └── page.tsx
└── page.tsx
Layout
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
notifications,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
notifications: React.ReactNode;
}) {
return (
<div>
<div>{children}</div>
<div>{analytics}</div>
<div>{notifications}</div>
</div>
);
}
8. Intercepting Routes
Modal pattern
app/
├── photos/
│ ├── page.tsx
│ └── [id]/
│ └── page.tsx
└── @modal/
└── (.)photos/
└── [id]/
└── page.tsx
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/Modal';
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<Modal>
<img src={`/photos/${params.id}.jpg`} alt="Photo" />
</Modal>
);
}
9. Hands-on example: blog
Below is a TypeScript example: dynamic metadata, revalidation, and notFound() for a blog post page.
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 60 },
});
if (!res.ok) {
return null;
}
return res.json();
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
if (!post) {
return {};
}
return {
title: post.title,
description: post.excerpt,
};
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
Wrap-up and checklist
Key takeaways
- App Router: the new routing model in Next.js 13+
- Server Components: render on the server
- Streaming: progressive rendering
- Server Actions: simpler form handling
- Parallel Routes: parallel segment rendering
- Intercepting Routes: modal-style flows
Implementation checklist
- Create an App Router project
- Understand Server Components
- Add loading and error UI
- Implement Server Actions
- Use Parallel Routes where useful
- Optimize metadata
- Deploy
Related reading
- React 18 deep dive
- Remix complete guide
- Prisma complete guide
Keywords covered in this post
Next.js, App Router, React, Server Components, Streaming, SSR, Full Stack
Frequently asked questions (FAQ)
Q. Should I migrate from the Pages Router?
A. Prefer the App Router for new projects. Existing apps can migrate gradually.
Q. Must I always use Server Components?
A. Default to Server Components; reach for Client Components only when you need interactivity.
Q. Will performance improve?
A. Yes—Server Components shrink the JavaScript bundle and speed up initial load.
Q. Is the learning curve steep?
A. Separating Server and Client Components can feel confusing at first, but it becomes intuitive with practice.