Skip to main content

Next.js App Router

Introduction

Next.js 13 introduced a powerful new routing system called the App Router. This innovative approach to routing is built on React Server Components and provides a more intuitive and flexible way to structure your web applications. The App Router uses a file-system based routing mechanism, where folders represent routes and special files handle UI components, loading states, error handling, and more.

In this guide, we'll explore the App Router's core concepts, learn how to implement various routing patterns, and understand how it differs from the traditional Pages Router in Next.js.

App Router vs Pages Router

Next.js now supports two routing systems:

  1. App Router: The new routing system (introduced in Next.js 13) built on React Server Components
  2. Pages Router: The original routing system in Next.js

Both can be used in the same project, but the App Router takes precedence. Let's focus on the App Router in this guide.

Core Concepts

File-System Based Routing

The App Router follows a file-system based routing structure:

  • Folders define routes
  • Files define UI
  • Nested folders create nested routes

All components inside the App Router are React Server Components by default, which allows for better performance and optimizations.

Getting Started with App Router

Project Structure

To start using the App Router, create an app directory at the root of your project:

my-next-app/
├── app/ # App Router
│ └── ...
├── pages/ # Pages Router (optional)
│ └── ...
├── public/
│ └── ...
├── package.json
└── next.config.js

Creating Your First Route

Let's create a simple home page:

jsx
// app/page.js
export default function HomePage() {
return (
<div>
<h1>Welcome to my Next.js App!</h1>
<p>This is the home page created with the App Router</p>
</div>
);
}

This creates a route for the path / (root of your website).

Nested Routes

To create nested routes, simply create nested folders:

app/
├── page.js # '/' route
├── dashboard/
│ └── page.js # '/dashboard' route
└── blog/
└── page.js # '/blog' route
jsx
// app/dashboard/page.js
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<p>This is the dashboard page</p>
</div>
);
}

When a user navigates to /dashboard, the above component will be rendered.

Special Files

The App Router introduces several special files that serve specific purposes:

FileDescription
page.jsUI for a route; makes the route publicly accessible
layout.jsShared UI for a segment and its children
loading.jsLoading UI for a segment
error.jsError UI for a segment
not-found.jsUI for "not found" errors
route.jsServer-side API endpoint

Layout Component

Layouts define shared UI for a segment and its children:

jsx
// app/layout.js
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/dashboard">Dashboard</a>
<a href="/blog">Blog</a>
</nav>
</header>
<main>{children}</main>
<footer>© {new Date().getFullYear()} My Next.js App</footer>
</body>
</html>
);
}

The children prop represents the child components of the layout - either a page or another layout.

Nested Layouts

You can create nested layouts by adding a layout.js file in a subfolder:

jsx
// app/blog/layout.js
export default function BlogLayout({ children }) {
return (
<div>
<aside>
<h3>Recent Posts</h3>
<ul>
<li><a href="/blog/post-1">Post 1</a></li>
<li><a href="/blog/post-2">Post 2</a></li>
</ul>
</aside>
<div>{children}</div>
</div>
);
}

In this example, the blog layout will be rendered within the root layout, and the specific blog page will be rendered as the children of the blog layout.

Dynamic Routes

Dynamic routes allow you to create paths that depend on external data, like a blog post ID or a product slug.

To create a dynamic route, use square brackets [] in the folder name:

app/
├── blog/
│ ├── page.js # '/blog' route
│ └── [slug]/
│ └── page.js # '/blog/:slug' route
jsx
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
// params.slug contains the dynamic value
return (
<div>
<h1>Blog Post: {params.slug}</h1>
<p>This is a dynamic route</p>
</div>
);
}

Generating Static Params

For static generation of dynamic routes, use generateStaticParams:

jsx
// app/blog/[slug]/page.js
export async function generateStaticParams() {
// This could come from a CMS, database, or file system
const posts = [
{ slug: 'introduction-to-nextjs' },
{ slug: 'advanced-routing' },
{ slug: 'server-components' }
];

return posts.map((post) => ({
slug: post.slug,
}));
}

export default function BlogPost({ params }) {
return (
<div>
<h1>Blog Post: {params.slug}</h1>
<p>This is a pre-rendered blog post</p>
</div>
);
}

Loading and Error States

Loading State

Create a loading.js file to show a loading state while the content is loading:

jsx
// app/dashboard/loading.js
export default function Loading() {
return (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading dashboard data...</p>
</div>
);
}

Error Handling

Create an error.js file to handle errors that occur in the route:

jsx
'use client'; // Error components must be Client Components

import { useEffect } from 'react';

export default function Error({ error, reset }) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);

return (
<div className="error-container">
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}

Route Handlers

Route handlers allow you to create custom API endpoints within the App Router:

jsx
// app/api/hello/route.js
export async function GET() {
return new Response(JSON.stringify({ message: 'Hello, world!' }), {
headers: { 'Content-Type': 'application/json' },
});
}

export async function POST(request) {
const body = await request.json();

return new Response(JSON.stringify({
message: `Hello, ${body.name || 'anonymous'}!`
}), {
headers: { 'Content-Type': 'application/json' },
});
}

For client-side navigation, use the Link component:

jsx
import Link from 'next/link';

export default function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/dashboard">Dashboard</Link>
<Link href="/blog">Blog</Link>
<Link href="/blog/first-post">First Post</Link>
</nav>
);
}

Programmatic Navigation

For programmatic navigation, use the useRouter hook:

jsx
'use client';

import { useRouter } from 'next/navigation';

export default function LoginForm() {
const router = useRouter();

const handleSubmit = (e) => {
e.preventDefault();
// Handle login logic
// ...

// Navigate to dashboard on success
router.push('/dashboard');
};

return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit">Login</button>
</form>
);
}

Real-World Example: Building a Blog

Let's combine these concepts into a more complete example of a simple blog:

Folder Structure

app/
├── layout.js
├── page.js (home)
├── blog/
│ ├── page.js (blog index)
│ ├── layout.js (blog layout)
│ └── [slug]/
│ └── page.js (blog post)
└── api/
└── posts/
└── route.js (API for posts)

Root Layout

jsx
// app/layout.js
import Link from 'next/link';
import './globals.css';

export const metadata = {
title: 'My Next.js Blog',
description: 'A blog built with Next.js App Router',
};

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>
<nav>
<Link href="/">Home</Link>
<Link href="/blog">Blog</Link>
</nav>
</header>
<main>{children}</main>
<footer>© {new Date().getFullYear()} My Next.js Blog</footer>
</body>
</html>
);
}

Home Page

jsx
// app/page.js
export default function HomePage() {
return (
<div className="container">
<h1>Welcome to My Blog</h1>
<p>This is a simple blog built with Next.js App Router</p>
<Link href="/blog" className="cta-button">
Read Blog Posts
</Link>
</div>
);
}

Blog Index Page

jsx
// app/blog/page.js
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5', {
next: { revalidate: 3600 } // Revalidate every hour
});

if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}

export default async function BlogPage() {
const posts = await getPosts();

return (
<div className="blog-container">
<h1>Blog Posts</h1>
<div className="posts-grid">
{posts.map(post => (
<article key={post.id} className="post-card">
<h2>{post.title}</h2>
<p>{post.body.substring(0, 100)}...</p>
<Link href={`/blog/${post.id}`}>Read more</Link>
</article>
))}
</div>
</div>
);
}

Blog Post Page

jsx
// app/blog/[slug]/page.js
async function getPost(id) {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
next: { revalidate: 3600 } // Revalidate every hour
});

if (!res.ok) {
// This will be caught by the error boundary
throw new Error('Failed to fetch post');
}

return res.json();
}

export async function generateStaticParams() {
const posts = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
.then(res => res.json());

return posts.map(post => ({
slug: post.id.toString(),
}));
}

export async function generateMetadata({ params }) {
const post = await getPost(params.slug);

return {
title: post.title,
description: post.body.substring(0, 160),
};
}

export default async function BlogPost({ params }) {
const post = await getPost(params.slug);

return (
<article className="blog-post">
<h1>{post.title}</h1>
<p className="post-body">{post.body}</p>
<Link href="/blog" className="back-button">
← Back to Blog
</Link>
</article>
);
}

Loading State for Blog

jsx
// app/blog/loading.js
export default function Loading() {
return (
<div className="loading-container">
<div className="pulse-loader"></div>
<p>Loading blog posts...</p>
</div>
);
}

Error Handling for Blog Post

jsx
// app/blog/[slug]/error.js
'use client';

import { useEffect } from 'react';
import Link from 'next/link';

export default function Error({ error, reset }) {
useEffect(() => {
console.error(error);
}, [error]);

return (
<div className="error-container">
<h2>Something went wrong loading this post!</h2>
<p>{error.message || 'An error occurred'}</p>
<div className="error-actions">
<button onClick={() => reset()} className="retry-button">
Try again
</button>
<Link href="/blog" className="back-link">
Return to Blog
</Link>
</div>
</div>
);
}

Summary

In this guide, we've explored the Next.js App Router, a powerful routing system built on React Server Components. We've learned:

  • The file-system based routing structure
  • How to create static and dynamic routes
  • How to implement layouts for shared UI
  • How to handle loading states and errors
  • How to create API route handlers
  • Client and server navigation techniques
  • How to build a real-world blog application

The App Router represents the future of Next.js routing with its improved performance, flexible layouts, and enhanced built-in features. It's designed to work with the latest React features like Server Components and Suspense, allowing you to build more performant and user-friendly applications.

Additional Resources

Exercises

  1. Create a personal portfolio site with the App Router that includes a home page, projects page, and a dynamic project details page.
  2. Implement a dashboard with nested layouts for different sections like analytics, settings, and user profile.
  3. Build a simple e-commerce product listing with dynamic product pages and API route handlers for product data.
  4. Create a blog with categories, where each category has its own layout and listing page.
  5. Add authentication to a dashboard and implement protected routes with middleware.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)