App Router vs Pages Router: What Changed?
The /app Directory: New Project Structure
Layouts: Persistent Shared UI
Templates: Re-render on Each Navigation
Pages: The Content of Each Route
Dynamic Routes: URL Parameters Made Easy
Catch-All & Optional Routes
Route Groups: Organize Without Affecting URLs
Parallel Routes
loading.tsx and Suspense
error.tsx and not-found.tsx
Metadata API (Bonus)
Building a Real App: Dashboard with Nested Routes
Performance Optimization Tips
Migrating from Pages to App Router
Conclusion
Next.js 14 has firmly established the App Router as the future of web development. Moving away from the classic Pages Router, the App Router empowers developers with nested layouts, React Server Components, streaming, error handling, and a new file-based routing system that unifies server and client logic like never before.
The App Router, introduced in Next.js 13 and stabilized in v14, offers a modern alternative to the legacy Pages Router.
Feature | App Router (/app) | Pages Router (/pages) |
---|---|---|
Layouts | ✅ Nested, persistent layouts | ❌ Manual |
Streaming | ✅ Built-in with Suspense | ❌ Limited |
Server Components | ✅ Default | ❌ Not supported |
Routing | ✅ File-based + nested | ✅ File-based only |
Metadata API | ✅ Modern, dynamic | ❌ Manual |
Client/Server boundaries | ✅ use 'use client' directive | ❌ All client-side |
Loading/Error UI | ✅ loading.tsx/error.tsx files | ❌ Needs manual handling |
Next.js App Router is designed for modern, scalable, full-stack web apps. It seamlessly combines server rendering with client interactivity and streamlines both frontend and backend logic.
With the App Router, your application’s structure begins in the /app directory.
Example:
/app ├── layout.tsx ├── page.tsx ├── loading.tsx ├── error.tsx ├── about/ │ ├── page.tsx │ └── layout.tsx ├── blog/ │ ├── [slug]/ │ │ ├── page.tsx │ │ ├── loading.tsx │ │ └── not-found.tsx │ └── page.tsx
Let’s break down the core file types:
Layouts define a common structure (e.g., navigation, sidebar, theme wrapper) and persist across routes. They are rendered once and stay mounted during route changes.
Example: /app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
You can nest layouts in subfolders for scoped structure:
/app/dashboard/layout.tsx → Scoped layout for dashboard routes
Unlike layouts, templates re-render every time a route is navigated to.
Use cases:
Example:
The template wraps gallery pages but resets when the route changes.
Each page.tsx file is rendered for a route.
Example:
You can use server or client components here.
By default, all components in /app are React Server Components.
To use client interactivity (e.g., event handlers, useState), add:
'use client';
Dynamic segments are defined with square brackets:
Example: /app/blog/[slug]/page.tsx
export default function BlogPost({ params }) {
return <h1>Post: {params.slug}</h1>;
}
params.slug is passed automatically from the URL.
Use catch-all segments:
Example:
/app/docs/[[...slug]]/page.tsx → Handles /docs and /docs/any/route
Want to group files without changing URLs?
Use parentheses:
Useful for separating authenticated and public layouts.
Advanced routing using @slot folders:
/app/(dashboard)/@analytics/page.tsx /app/(dashboard)/@messages/page.tsx
You can render multiple routes in parallel inside a single layout.
This is useful for:
Show skeletons or spinners while data loads.
Example:
/app/blog/[slug]/loading.tsx:
export default function Loading() {
return <p>Loading blog post...</p>;
}
Works with React Suspense:
<Suspense fallback={<Loading />}>
<BlogPost />
</Suspense>
Handle errors gracefully:
/app/error.tsx
'use client';
export default function GlobalError({ error, reset }) {
return (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
/app/blog/[slug]/not-found.tsx → Custom 404 page for that segment
Trigger programmatically:
import { notFound } from 'next/navigation';
notFound();
Set SEO metadata per page or layout:
/app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.summary,
};
}
Works with Open Graph, Twitter Cards, and canonical URLs.
Structure:
/app ├── layout.tsx (site layout) ├── dashboard/ │ ├── layout.tsx (sidebar layout) │ ├── page.tsx (dashboard home) │ ├── users/ │ │ └── page.tsx │ ├── settings/ │ └── page.tsx
Each layout is scoped: the sidebar persists across dashboard routes, but not the main site.
Steps:
The App Router in Next.js 14 represents a fundamental evolution in web application architecture. It blends the power of server rendering, dynamic routing, component streaming, and full-stack logic, all into a unified developer experience.
Key benefits include:
Whether you're building a blog, dashboard, SaaS platform, or an eCommerce site, mastering the App Router is a must for building scalable and performant apps in 2025 and beyond.
So go ahead, embrace the new architecture, refactor your routes, and start building like a pro with the App Router!
Software Engineer
Senior Software Engineer