Understanding Next.js App Router - A Friendly Guide for Real Developers
Next.jsApp RouterReact

Understanding Next.js App Router - A Friendly Guide for Real Developers

January 23, 2025
11 min read
Salman Izhar

Understanding Next.js App Router - A Friendly Guide for Real Developers

When Next.js 13 dropped the App Router, I was skeptical.

"Why change something that works?" I thought. The Pages Router was fine. Familiar. It did the job.

Then I actually tried the App Router on a real project.

And I got it.

The App Router isn't just a different way to do routing. It's a complete rethink of how we build Next.js apps. And once you understand it, you'll never want to go back.

App Router vs Pages Router: What Changed?

The Old Way (Pages Router)

pages/
├── index.js          → /
├── about.js          → /about
├── blog/
│   └── [slug].js     → /blog/:slug
└── _app.js           → Root component

Simple file-based routing. One file = one route. Easy to understand.

The New Way (App Router)

app/
├── page.tsx          → /
├── layout.tsx        → Shared layout
├── about/
│   └── page.tsx      → /about
└── blog/
    └── [slug]/
        └── page.tsx  → /blog/:slug

Folders for routes. Special files for different purposes. More structure, more power.

Why the Change Makes Sense

The Pages Router had problems:

  • Shared layouts required awkward _app.js hacks
  • Loading states meant custom logic
  • Error boundaries were painful
  • Everything was client-side by default

App Router fixes all of this.

Core Concepts: What You Need to Know

1. Folders Define Routes

In App Router, folders create URL segments.

app/
├── dashboard/        → /dashboard
│   └── settings/     → /dashboard/settings
│       └── page.tsx

The page.tsx file makes it a route. Without it, the folder is just for organization.

2. Special Files with Special Powers

Each folder can have these special files:

  • page.tsx - The UI for that route
  • layout.tsx - Shared layout that wraps children
  • loading.tsx - Loading UI (automatic Suspense)
  • error.tsx - Error boundary
  • not-found.tsx - 404 page
  • template.tsx - Like layout, but re-renders on navigation
app/
└── dashboard/
    ├── page.tsx        → Main UI
    ├── layout.tsx      → Wraps all dashboard pages
    ├── loading.tsx     → Shows while loading
    └── error.tsx       → Catches errors

3. Layouts: The Game Changer

Layouts are persistent. They don't re-render when you navigate between pages.

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    
{children}
) }

Now every page inside app/dashboard/* automatically gets this layout. The sidebar stays mounted. State persists. No flicker.

4. Nested Layouts

Layouts stack. Each level can have its own.

app/
├── layout.tsx              → Root layout (all pages)
└── dashboard/
    ├── layout.tsx          → Dashboard layout
    └── settings/
        ├── layout.tsx      → Settings layout
        └── page.tsx

When you visit /dashboard/settings:

  • Root layout wraps everything
  • Dashboard layout wraps settings
  • Settings layout wraps the page

Three layers. All automatic.

Server Components by Default

Here's the big shift: everything is a Server Component unless you opt out.

// This runs on the server
export default async function BlogPost({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
    .then(r => r.json())
  
  return (
    

{post.title}

{post.content}

) }

No useEffect. No loading states. Just fetch and render. The HTML is sent to the browser fully rendered.

When You Need Client Components

Add 'use client' when you need:

  • State (useState, useReducer)
  • Effects (useEffect)
  • Event handlers (onClick, onChange)
  • Browser APIs (localStorage, window)
  • Context
'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  
  return (
    
  )
}

Loading States Made Easy

Remember adding loading spinners everywhere? Not anymore.

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    
) }

That's it. Next.js automatically shows this while page.tsx loads. No Suspense wrapping. No manual state. It just works.

Error Handling Built In

// app/dashboard/error.tsx
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    

Something went wrong!

{error.message}

) }

Any error in dashboard pages gets caught here. Automatic error boundaries for every route.

Dynamic Routes

Dynamic routes work similarly, but the syntax changed slightly.

app/
└── blog/
    └── [slug]/
        └── page.tsx    → /blog/anything

Access params in the component:

export default async function BlogPost({
  params,
}: {
  params: { slug: string }
}) {
  const post = await getPost(params.slug)
  
  return 
{post.content}
}

Catch-All Routes

app/
└── docs/
    └── [...slug]/
        └── page.tsx    → /docs/anything/any/depth

Optional Catch-All

app/
└── shop/
    └── [[...slug]]/
        └── page.tsx    → /shop AND /shop/anything

Route Groups

Want to organize routes without affecting URLs?

app/
├── (marketing)/
│   ├── about/
│   └── contact/
└── (dashboard)/
    ├── settings/
    └── profile/

The parentheses mean the folder name isn't in the URL:

  • /about (not /marketing/about)
  • /settings (not /dashboard/settings)

Great for organization and different layouts.

// app/(marketing)/layout.tsx
export default function MarketingLayout({ children }) {
  return (
    
{children}
) } // app/(dashboard)/layout.tsx export default function DashboardLayout({ children }) { return (
{children}
) }

Different layouts. Same URL structure.

Parallel Routes

This is advanced, but powerful. Show multiple pages in one layout.

app/
└── dashboard/
    ├── @analytics/
    │   └── page.tsx
    ├── @revenue/
    │   └── page.tsx
    └── layout.tsx
// app/dashboard/layout.tsx
export default function Layout({
  children,
  analytics,
  revenue,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  revenue: React.ReactNode
}) {
  return (
    
{analytics}
{revenue}
{children}
) }

Three independent sections. Each can load separately. Show different loading/error states.

Metadata for SEO

Pages Router needed next-seo or manual tags. App Router has built-in metadata.

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'

export async function generateMetadata({ params }): Promise {
  const post = await getPost(params.slug)
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.image],
    },
  }
}

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

Dynamic metadata based on the page data. Perfect for SEO.

Migration: Should You Switch?

Stick with Pages Router if:

  • Your app works fine
  • You're close to shipping
  • Team doesn't want to learn new patterns

Switch to App Router if:

  • Starting a new project
  • Need better layouts
  • Want server components
  • SEO is critical

You can use both in the same project during migration.

Real-World Example: Dashboard

Here's a complete dashboard structure:

app/
├── layout.tsx                  → Root
├── (auth)/
│   ├── login/
│   │   └── page.tsx
│   └── layout.tsx              → Auth pages layout
└── (dashboard)/
    ├── layout.tsx              → Dashboard layout
    ├── loading.tsx             → Dashboard loading
    ├── error.tsx               → Dashboard errors
    ├── page.tsx                → /dashboard
    ├── settings/
    │   ├── page.tsx            → /dashboard/settings
    │   ├── profile/
    │   │   └── page.tsx        → /dashboard/settings/profile
    │   └── security/
    │       └── page.tsx        → /dashboard/settings/security
    └── analytics/
        └── page.tsx            → /dashboard/analytics

Clean organization. Proper layouts. Automatic loading/error handling.

The Bottom Line

App Router feels weird at first. More files. New patterns. Different mental model.

But once it clicks:

  • Layouts are easy
  • Loading states are automatic
  • Errors are handled
  • SEO just works
  • Server components are fast

It's not just different. It's better.

Give it a real try. Build something. You'll see why Next.js made this change.

---

Ready to dive deeper? Check out my guide on how Context API fits into modern Next.js App Router projects.
Get More Like This

Want articles like this in your inbox?

Join developers and founders who get practical insights on frontend, SaaS, and building better products.

S

Written by Salman Izhar

Frontend Developer specializing in React, Next.js, and building high-converting web applications.

Learn More