The Complete Next.js App Router Guide | Server Components, Streaming, Parallel Routes

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

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

  • 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.