Next.js App Router Guide: Core Concepts and Basic Usage
Introduction
To be honest, I was confused the first time I opened the Next.js official documentation.
In the left sidebar, “Pages Router” and “App Router” stood side by side, as if saying “take your pick.” But here’s the problem—which one should I choose? What’s the difference between them? Which one should I learn? The docs didn’t give me a straight answer, and I got more confused. Some tutorials use the pages folder, others use the app folder, and the code is completely different.
Later I figured it out: Next.js actually has two completely different routing systems. The old one is called Pages Router, stable and reliable, but some new features aren’t available. The new one is called App Router, introduced from v13, officially stable from v13.4, and now it’s the officially recommended direction.
You might ask: “Do I need to learn App Router? Isn’t it just another hassle?”
This article is here to help you with that confusion. I’ll explain the core concepts of App Router in the simplest way—what are Server Components, how to use special files, and how it differs from Pages Router. After reading, you’ll be able to get started quickly and avoid detours.
What is App Router? Why Use It?
Simply put, App Router is the new routing system introduced in Next.js v13. It’s based on React’s latest features—Server Components—and is designed to be more modern and flexible.
Compared to the old Pages Router, there are three obvious benefits:
1. Better Performance
App Router uses server components by default. This means most code runs on the server, users download less JavaScript, and pages load faster. According to Vercel’s 2024 report, over 60% of top Next.js applications have switched to App Router.
"Over 60% of top Next.js applications have switched to App Router, making it the officially recommended direction for new projects."
2. More Flexible Layout System
In Pages Router, implementing nested layouts is quite troublesome. App Router handles it directly with layout.js files, and layouts don’t re-render when switching pages—smooth experience.
3. More Powerful Error Handling and Loading States
You can use loading.js files to define loading animations and error.js to catch errors and display fallback UI. These need to be hand-coded in Pages Router, but App Router has them built in.
Can Pages Router still be used? Yes.
The two systems can coexist, but if you’re learning Next.js now, I suggest starting with App Router. It’s the official recommendation, and new project scaffolding (starting from v14.1.4) uses App Router by default.
File System Routing: From Directories to Pages
The core concept of App Router is: Your folder structure is your routing structure.
Sounds a bit abstract, but look at an example:
app/
├── page.js # Home page, maps to /
├── about/
│ └── page.js # About page, maps to /about
└── blog/
├── page.js # Blog list, maps to /blog
└── [slug]/
└── page.js # Blog detail, maps to /blog/:slug
Here are a few key points to remember:
1. page.js is the route entry
Only files named page.js will become accessible pages. Other files (like layout.js, loading.js) are supporting functional files and won’t be directly accessed.
2. Dynamic routes use square brackets
Want a dynamic route like /blog/hello-world? Create app/blog/[slug]/page.js, and the slug parameter will be automatically passed to your component:
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
return <h1>Article: {params.slug}</h1>
}
3. Catch all routes with [...slug]
Sometimes you need to match multi-level paths like /docs/a/b/c. Use app/docs/[...slug]/page.js, and params.slug will be an array ['a', 'b', 'c'].
Comparing with Pages Router:
If you’ve used Pages Router before, you’ll notice it uses pages/blog/[id].js. App Router changes this to app/blog/[id]/page.js, adding an extra folder level. Why? To leave room for each route to have layout.js, loading.js, and other special files.
It might seem tedious at first, but once you get used to it—the whole project structure is much clearer.
Server Components vs Client Components: Core Concepts
This might be the most confusing concept in App Router. I was confused for a long time when I first started learning.
Simply put: Components in App Router run on the server by default, and only run in the browser when interaction is needed.
Server Component by Default
Components created under the app/ directory are Server Components by default. They render on the server and send HTML directly to the browser.
The benefits are obvious:
- Smaller JavaScript bundle: Code doesn’t need to be sent to the browser, users download smaller JS files
- Direct access to backend resources: Database queries, API keys, and other sensitive information can be used safely
- Faster first paint: Server renders and sends it directly, shorter FCP (First Contentful Paint) time
"Server Components render on the server and send HTML directly to the browser, resulting in smaller JavaScript bundles and faster first paint."
Here’s an example of a typical Server Component:
// app/products/page.js
// This is a Server Component, runs on the server
async function getProducts() {
const res = await fetch('https://api.example.com/products')
return res.json()
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
<h1>Product List</h1>
{products.map(p => (
<div key={p.id}>{p.name}</div>
))}
</div>
)
}
See? You can directly use async/await to fetch data, no need for useEffect or getServerSideProps.
When to Use Client Component?
But some scenarios must run in the browser, such as:
- Need to use React hooks (
useState,useEffect) - Need to handle user interactions (
onClick,onChange) - Need to use browser APIs (
localStorage,window)
That’s when you need Client Component. Marking is simple, add a line at the top of the file: 'use client':
// components/AddToCartButton.js
'use client' // Mark as Client Component
import { useState } from 'react'
export default function AddToCartButton({ productId }) {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Add to Cart ({count})
</button>
)
}
Mixing Both: Best Practice
The really powerful thing is that you can mix both types of components.
For example, a product page:
- Product list uses Server Component (fetch data server-side, reduce JS bundle)
- Add to cart button uses Client Component (needs to handle click interactions)
// app/products/page.js (Server Component)
import AddToCartButton from '@/components/AddToCartButton' // Client Component
async function getProducts() {
// Fetch data server-side
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
<h1>Product List</h1>
{products.map(p => (
<div key={p.id}>
{p.name}
<AddToCartButton productId={p.id} />
</div>
))}
</div>
)
}
Remember one principle: Use Server Component by default, it’s enough. Only use 'use client' when you really need interaction.
Don’t just add 'use client' to every component from the start—what’s the difference from not using App Router?
Special Files: Making Your Project More Professional
App Router defines a bunch of special file names—layout.js, loading.js, error.js, etc. They might seem troublesome at first, but they’re really nice to use.
layout.js: Shared Layout
This is the most commonly used special file. It defines the layout of a route segment and wraps all pages at the same level and below.
For example, if you want to add a navbar and footer to the entire app:
// app/layout.js (Root layout)
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<nav>Navbar</nav>
<main>{children}</main>
<footer>Footer</footer>
</body>
</html>
)
}
Even better, you can nest layouts:
app/
├── layout.js # Global layout (navbar + footer)
├── page.js # Home page
└── dashboard/
├── layout.js # Dashboard layout (sidebar)
├── page.js # /dashboard
└── settings/
└── page.js # /dashboard/settings
When you navigate from /dashboard to /dashboard/settings, the global layout and dashboard layout won’t re-render—only page.js updates. Smooth.
loading.js: Loading State
No need to write your own useState to manage loading. Create a loading.js, and App Router will automatically wrap your page with Suspense:
// app/dashboard/loading.js
export default function Loading() {
return <div>Loading...</div>
}
When the page fetches data, loading.js content will automatically display. That simple.
error.js: Error Boundary
Used to catch page errors and display fallback UI:
// app/dashboard/error.js
'use client' // Error boundaries must be Client Components
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong: {error.message}</h2>
<button onClick={reset}>Retry</button>
</div>
)
}
Watch out for this gotcha: error.js cannot catch errors from the same-level layout.js. Why? React Error Boundary limitation—it can only catch child component errors, not its own or parent errors.
To catch layout.js errors, you need to either put error.js in the parent directory or use global-error.js in the root directory.
not-found.js: 404 Page
Displayed when a route doesn’t exist:
// app/not-found.js
export default function NotFound() {
return <h1>Page Not Found</h1>
}
You can also manually trigger 404 in code:
import { notFound } from 'next/navigation'
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
if (!post) notFound() // Trigger not-found.js
return <article>{post.title}</article>
}
File Hierarchy
These special files have a fixed hierarchy:
layout.js
├── loading.js (Suspense boundary)
│ └── page.js
└── error.js (Error boundary)
layout is at the outermost level, and error.js can’t wrap it. loading.js handles loading state, error.js handles errors.
Understand this hierarchy, and you won’t step into pitfalls.
Data Fetching: Goodbye getServerSideProps
If you’ve used Pages Router, you’ve definitely written getServerSideProps or getStaticProps. Honestly, that API is awkward—you have to export a separate function, and data passing isn’t intuitive.
App Router simplifies all of this.
Direct async/await
In Server Components, you can fetch data directly in the component function:
// app/posts/page.js
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 (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
See? Just regular async/await, no special API needed.
Parallel Data Fetching
Even better, you can fetch from multiple data sources in parallel:
export default async function Dashboard() {
// Fetch in parallel, non-blocking
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats()
])
return (
<div>
<h1>{user.name}</h1>
<Posts data={posts} />
<Stats data={stats} />
</div>
)
}
Data Caching and Revalidation
Next.js automatically caches fetch requests. You can control the caching strategy:
// Cache for 60 seconds then revalidate
fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
// No cache, fetch fresh data every time
fetch('https://api.example.com/data', {
cache: 'no-store'
})
Comparing with Pages Router:
- Pages Router:
getServerSideProps+getStaticProps, need separate function export - App Router: Direct
async/await, fetch data inside component
Much simpler, right?
Common Beginner Questions and Solutions
When learning App Router, I stepped into quite a few pitfalls. Here are the most common questions I’ve compiled, hoping to help you avoid detours.
Question 1: When to use ‘use client’?
Confusion: Seeing 'use client' everywhere in tutorials, not knowing when to add it.
Solution:
Remember one principle—don’t add by default, add only when needed.
Only add 'use client' in these situations:
- Using React hooks (
useState,useEffect,useContext) - Handling user interactions (
onClick,onChange) - Using browser APIs (
window,localStorage)
Don’t add it other times. Server Component performs better and can directly access backend resources.
Question 2: Relationship between layout.js and page.js?
Confusion: These two files are in the same folder, who wraps whom?
Solution:
layout.js wraps page.js and child routes.
app/
├── layout.js # Wraps all pages below
├── page.js # Home, wrapped by above layout
└── about/
└── page.js # About page, also wrapped by above layout
When switching pages, layout.js won’t re-render—only page.js updates. That’s why the navbar doesn’t flicker.
Question 3: How to get dynamic route parameters?
Confusion: Created [slug]/page.js but don’t know how to get the slug value.
Solution:
Get it through the params prop:
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
console.log(params.slug) // The value in the URL
return <h1>Article: {params.slug}</h1>
}
For nested dynamic routes, like app/blog/[category]/[slug]/page.js:
export default function Post({ params }) {
console.log(params.category, params.slug)
return <h1>{params.category} - {params.slug}</h1>
}
Question 4: error.js not working?
Confusion: Created error.js but it doesn’t catch layout errors.
Solution:
error.js cannot catch errors from the same-level layout.js. This is a React Error Boundary limitation.
To catch layout errors, there are two approaches:
- Put
error.jsin the parent directory - Use
global-error.jsin the root directory (must include<html>and<body>tags)
// app/global-error.js
'use client'
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<h2>Global error: {error.message}</h2>
<button onClick={reset}>Retry</button>
</body>
</html>
)
}
Question 5: Should I migrate my old project?
Confusion: Seeing all these new things in App Router, worried old projects need complete rewrites.
Solution:
No rush.
Pages Router and App Router can coexist. You can:
- Keep old features in
pages/ - Use
app/for new features
Vercel officially said Pages Router will be supported long-term and won’t be deprecated.
But for new projects, go straight for App Router. It’s the future direction, and the ecosystem will keep getting better.
Getting Started with Next.js App Router
Complete guide to set up and use Next.js App Router in a new project
⏱️ Estimated time: 30 min
- 1
Step1: Create a new Next.js project with App Router
Create a new Next.js project using the latest version (v14.1.4+ uses App Router by default):
• npx create-next-app@latest my-app
• Select "Yes" for App Router when prompted
• Choose your preferred options (TypeScript, ESLint, Tailwind CSS, etc.)
The project will have an app/ directory instead of pages/ directory. - 2
Step2: Understand file system routing structure
Create routes by organizing files in the app/ directory:
• app/page.js → Home page (/)
• app/about/page.js → About page (/about)
• app/blog/[slug]/page.js → Dynamic route (/blog/:slug)
• app/docs/[...slug]/page.js → Catch-all route (/docs/*)
Key points:
• Only page.js files become accessible routes
• Use square brackets for dynamic routes: [slug]
• Use three dots for catch-all: [...slug]
• Other files (layout.js, loading.js) are supporting files - 3
Step3: Create a root layout
Create app/layout.js for the root layout (required in App Router):
// app/layout.js
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<nav>Navigation</nav>
<main>{children}</main>
<footer>Footer</footer>
</body>
</html>
)
}
This layout wraps all pages. You can nest layouts for different sections. - 4
Step4: Use Server Components by default
Components in app/ directory are Server Components by default:
// app/products/page.js
async function getProducts() {
const res = await fetch('https://api.example.com/products')
return res.json()
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
{products.map(p => <div key={p.id}>{p.name}</div>)}
</div>
)
}
Benefits:
• Direct async/await for data fetching
• No need for getServerSideProps
• Smaller JavaScript bundle
• Faster page load - 5
Step5: Add Client Components when needed
Mark components as Client Components only when you need:
• React hooks (useState, useEffect)
• User interactions (onClick, onChange)
• Browser APIs (window, localStorage)
// components/AddToCartButton.js
'use client' // Add this directive
import { useState } from 'react'
export default function AddToCartButton({ productId }) {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Add to Cart ({count})
</button>
)
}
Remember: Use Server Components by default, only add 'use client' when necessary. - 6
Step6: Add special files for better UX
Create special files for enhanced functionality:
1. Loading state: app/dashboard/loading.js
export default function Loading() {
return <div>Loading...</div>
}
2. Error handling: app/dashboard/error.js
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>Error: {error.message}</h2>
<button onClick={reset}>Retry</button>
</div>
)
}
3. 404 page: app/not-found.js
export default function NotFound() {
return <h1>Page Not Found</h1>
}
These files are automatically used by Next.js. - 7
Step7: Handle dynamic route parameters
Access dynamic route parameters through the params prop:
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
// params.slug contains the value from URL
return <h1>Article: {params.slug}</h1>
}
For nested dynamic routes:
// app/blog/[category]/[slug]/page.js
export default function Post({ params }) {
// Access both: params.category and params.slug
return <h1>{params.category} - {params.slug}</h1>
}
Note: params is available in Server Components directly.
Conclusion
After all that, let’s quickly recap the five core concepts of App Router:
- File System Routing: Folder structure is routing structure,
page.jsis the entry - Server Components: Run server-side by default, better performance
- Client Components: Mark with
'use client'when interaction is needed - Special Files:
layout.js,loading.js,error.jsmake projects more professional - Data Fetching: Direct
async/await, goodbyegetServerSideProps
App Router is indeed the future direction of Next.js. Vercel continues to invest, and the community is actively keeping up. If you’re starting to learn Next.js now, going straight for App Router is the right choice.
What’s next?
Get your hands dirty. Create a small project and try building a blog or todo app with App Router. No amount of concepts beats typing the code yourself.
Don’t panic when you hit problems—Pages Router and App Router can coexist. If you really can’t figure it out, use Pages Router first and migrate gradually.
Finally, though the Next.js official docs are a bit messy, the App Router section is quite detailed. When you run into specific issues, remember to check the docs or search on GitHub Discussions.
Good luck with your learning!
FAQ
When should I use 'use client' in App Router?
• React hooks (useState, useEffect, useContext)
• User interactions (onClick, onChange, onSubmit)
• Browser APIs (window, localStorage, document)
By default, components are Server Components which perform better. Don't add 'use client' to every component—only when interaction is required.
What's the relationship between layout.js and page.js?
Example structure:
app/
├── layout.js (wraps everything below)
├── page.js (home page)
└── about/
└── page.js (also wrapped by root layout)
How do I get dynamic route parameters in App Router?
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
return <h1>Article: {params.slug}</h1>
}
For nested dynamic routes, access all parameters:
// app/blog/[category]/[slug]/page.js
export default function Post({ params }) {
return <h1>{params.category} - {params.slug}</h1>
}
Why isn't my error.js catching layout errors?
To catch layout errors, you have two options:
1. Place error.js in the parent directory
2. Use global-error.js in the root directory (must include <html> and <body> tags)
Should I migrate my existing Pages Router project to App Router?
• Keep existing features in pages/ directory
• Use app/ directory for new features
• Migrate gradually
Vercel has officially stated that Pages Router will be supported long-term and won't be deprecated. However, for new projects, App Router is the recommended direction.
How is data fetching different in App Router compared to Pages Router?
Pages Router:
• Need to export getServerSideProps or getStaticProps
• Data passing through props is less intuitive
• Separate function for data fetching
App Router:
• Direct async/await in Server Components
• Fetch data directly in component function
• Built-in caching with fetch API
• Parallel fetching with Promise.all
Much simpler and more intuitive!
Can I use both Pages Router and App Router in the same project?
• Keep existing pages/ routes working
• Add new features using app/ directory
• Migrate routes one by one when ready
This flexibility makes migration less risky and allows you to adopt App Router incrementally.
10 min read · Published on: Dec 18, 2025 · Modified on: Jan 22, 2026
Related Posts
Next.js E-commerce in Practice: Complete Guide to Shopping Cart and Stripe Payment Implementation
Next.js E-commerce in Practice: Complete Guide to Shopping Cart and Stripe Payment Implementation
Complete Guide to Next.js File Upload: S3/Qiniu Cloud Presigned URL Direct Upload
Complete Guide to Next.js File Upload: S3/Qiniu Cloud Presigned URL Direct Upload
Next.js Unit Testing Guide: Complete Jest + React Testing Library Setup

Comments
Sign in with GitHub to leave a comment