bangden.id/blog/nextjs-app-router-guide
TutorialMar 10, 20243 min read

Next.js App Router: Complete Guide

Everything you need to know about the App Router. From Server Components to streaming, caching strategies, and migration tips from Pages Router.

Next.js App Router: Complete Guide
BD

Bang Den

Frontend Engineer & Designer

The New Mental Model

The App Router fundamentally changes how we think about React applications. Server Components are now the default—and that's a good thing.

Server vs Client Components

Server Components (Default)

TypeScript (TSX)
1// app/posts/page.tsx
2// This runs ONLY on the server
3async function PostsPage() {
4 const posts = await db.posts.findMany(); // Direct DB access!
5
6 return (
7 <div>
8 {posts.map(post => (
9 <PostCard key={post.id} post={post} />
10 ))}
11 </div>
12 );
13}

Client Components

TypeScript (TSX)
1'use client';
2
3import { useState } from 'react';
4
5export function LikeButton({ postId }: { postId: string }) {
6 const [likes, setLikes] = useState(0);
7
8 return (
9 <button onClick={() => setLikes(l => l + 1)}>
10 ❤️ {likes}
11 </button>
12 );
13}

Data Fetching Patterns

Parallel Data Fetching

TypeScript (TSX)
1async function Dashboard() {
2 // These run in parallel!
3 const [user, posts, analytics] = await Promise.all([
4 getUser(),
5 getPosts(),
6 getAnalytics(),
7 ]);
8
9 return (
10 <div>
11 <UserProfile user={user} />
12 <PostsList posts={posts} />
13 <AnalyticsChart data={analytics} />
14 </div>
15 );
16}

Sequential with Suspense

TypeScript (TSX)
1import { Suspense } from 'react';
2
3export default function Page() {
4 return (
5 <div>
6 <h1>Dashboard</h1>
7
8 <Suspense fallback={<UserSkeleton />}>
9 <UserProfile />
10 </Suspense>
11
12 <Suspense fallback={<PostsSkeleton />}>
13 <RecentPosts />
14 </Suspense>
15 </div>
16 );
17}

Route Handlers (API Routes)

TYPESCRIPT
1// app/api/posts/route.ts
2import { NextResponse } from 'next/server';
3
4export async function GET() {
5 const posts = await db.posts.findMany();
6 return NextResponse.json(posts);
7}
8
9export async function POST(request: Request) {
10 const body = await request.json();
11 const post = await db.posts.create({ data: body });
12 return NextResponse.json(post, { status: 201 });
13}

Caching Strategies

Static vs Dynamic

TypeScript (TSX)
1// Force static generation
2export const dynamic = 'force-static';
3
4// Force dynamic rendering
5export const dynamic = 'force-dynamic';
6
7// Revalidate every 60 seconds
8export const revalidate = 60;

On-Demand Revalidation

TYPESCRIPT
1// app/api/revalidate/route.ts
2import { revalidatePath, revalidateTag } from 'next/cache';
3
4export async function POST(request: Request) {
5 const { path, tag } = await request.json();
6
7 if (path) {
8 revalidatePath(path);
9 }
10
11 if (tag) {
12 revalidateTag(tag);
13 }
14
15 return Response.json({ revalidated: true });
16}

Metadata API

TypeScript (TSX)
1// app/blog/[slug]/page.tsx
2import { Metadata } from 'next';
3
4export async function generateMetadata({ params }): Promise<Metadata> {
5 const post = await getPost(params.slug);
6
7 return {
8 title: post.title,
9 description: post.excerpt,
10 openGraph: {
11 images: [post.coverImage],
12 },
13 };
14}

Loading & Error States

TypeScript (TSX)
1// app/posts/loading.tsx
2export default function Loading() {
3 return <PostsSkeleton />;
4}
5
6// app/posts/error.tsx
7'use client';
8
9export default function Error({ error, reset }) {
10 return (
11 <div>
12 <h2>Something went wrong!</h2>
13 <button onClick={() => reset()}>Try again</button>
14 </div>
15 );
16}

Conclusion

The App Router is a paradigm shift. Embrace Server Components, use streaming wisely, and your apps will be faster than ever.

Share this article

© 2026 Bang Den. Built with and Tailwind.