Next.js App Router — Complete Guide to File-Based Routing

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Introduction

The Next.js App Router, introduced in Next.js 13 and stable in Next.js 14+, fundamentally changes how you structure applications. Instead of a pages directory, you use an app directory where folders define routes and special files like page.tsx, layout.tsx, and loading.tsx control rendering behavior.

Why This Matters

The App Router brings React Server Components (RSC) into the routing layer, enabling components to fetch data directly on the server without client-side JavaScript overhead. This reduces Time to First Byte (TTFB), shrinks JavaScript bundle sizes, and improves Core Web Vitals scores.

Nested layouts re-render only the segments that change on navigation — a sidebar stays mounted while the main content swaps. This eliminates full-page reloads and creates app-like navigation experiences with minimal effort.

By 2025, the App Router is the recommended default for all new Next.js projects. Understanding its conventions is essential for building performant, SEO-optimized Next.js applications.

File-Based Routing Conventions

The App Router maps the folder structure inside app/ directly to URL paths. A page.tsx file makes a segment publicly accessible:

app/
├── page.tsx              → /
├── about/
│   └── page.tsx          → /about
├── blog/
│   ├── page.tsx          → /blog
│   └── [slug]/
│       └── page.tsx      → /blog/:slug
└── api/
    └── posts/
        └── route.ts      → /api/posts

Special files at each level control behavior:

FilePurpose
page.tsxRenders the route UI
layout.tsxPersistent wrapper, does not re-render
loading.tsxSuspense fallback shown while data loads
error.tsxError boundary for the segment
not-found.tsxRendered when notFound() is called
route.tsAPI endpoint (GET, POST, etc.)

Creating Pages and Layouts

// app/page.tsx — home route
export default function HomePage() {
  return <h1>Welcome to My Site</h1>
}
// app/layout.tsx — root layout (required)
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Layouts wrap child segments. The root layout must include html and body tags. Nested layouts compose automatically:

// app/blog/layout.tsx — applies to /blog and all sub-routes
export default function BlogLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex gap-8">
      <aside className="w-64">
        <nav>
          <a href="/blog">All Posts</a>
          <a href="/blog/tutorials">Tutorials</a>
        </nav>
      </aside>
      <main className="flex-1">{children}</main>
    </div>
  )
}

Dynamic Routes

Dynamic segments capture variable URL parts using square brackets:

// app/blog/[slug]/page.tsx
export default function BlogPost({
  params,
}: {
  params: { slug: string }
}) {
  return <article><h1>Post: {params.slug}</h1></article>
}

For multiple dynamic segments: app/shop/[category]/[product]/page.tsx captures both params.category and params.product.

Catch-all routes with [...slug] match any number of segments:

// app/docs/[...slug]/page.tsx
// Matches: /docs/intro, /docs/api/users, /docs/a/b/c
export default function DocsPage({ params }: { params: { slug: string[] } }) {
  const path = params.slug.join('/')
  return <div>Docs: {path}</div>
}

Optional catch-all with [[...slug]] also matches the root segment (/docs).

Route Groups

Wrap folder names in parentheses to group routes without affecting the URL:

app/
├── (marketing)/
│   ├── layout.tsx        → marketing layout
│   ├── page.tsx          → /
│   └── about/page.tsx    → /about
└── (dashboard)/
    ├── layout.tsx        → dashboard layout
    └── analytics/page.tsx → /analytics

Route groups let you apply different layouts to different sections without URL nesting. Useful for separating authenticated and public pages.

Parallel Routes

Render multiple pages simultaneously within the same layout using @folder slots:

// app/layout.tsx
export default function Layout({
  children,
  team,
  analytics,
}: {
  children: React.ReactNode
  team: React.ReactNode
  analytics: React.ReactNode
}) {
  return (
    <div>
      {children}
      <div className="grid grid-cols-2">
        {team}
        {analytics}
      </div>
    </div>
  )
}
// app/@team/page.tsx and app/@analytics/page.tsx render independently

Programmatic Navigation

'use client'
 
import { useRouter } from 'next/navigation'
 
export function LoginButton() {
  const router = useRouter()
 
  async function handleLogin() {
    await signIn()
    router.push('/dashboard')
    router.refresh() // Re-fetch server data
  }
 
  return <button onClick={handleLogin}>Log In</button>
}

Use redirect() from next/navigation inside Server Components and Server Actions for server-side redirects.

generateStaticParams for Static Generation

Pre-generate dynamic routes at build time:

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())
  return posts.map((post: { slug: string }) => ({ slug: post.slug }))
}
 
export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json())
  return <article>{post.content}</article>
}

Common Mistakes

  • Forgetting page.tsx — a folder without it is not a public route
  • Placing layout.tsx one level too high or low, causing wrong UI nesting
  • Using useRouter from next/router (Pages Router) instead of next/navigation (App Router)
  • Not adding 'use client' to components that use hooks or browser APIs
  • Confusing route groups (name) with dynamic segments [name]

Best Practices

  • Keep layouts as Server Components — only add 'use client' when you need interactivity
  • Use route groups to separate concerns: (auth), (marketing), (app)
  • Prefer Link over programmatic navigation for SEO and prefetching
  • Use generateStaticParams for content-heavy routes to pre-render at build time
  • Add loading.tsx at every data-fetching level for instant visual feedback
  • Define generateMetadata per page for dynamic, SEO-accurate title and description tags

Key Takeaways

  • The App Router is stable since Next.js 14 and recommended for all new projects
  • page.tsx makes a folder a public route; layout.tsx wraps it without re-rendering on navigation
  • Dynamic routes use [param], catch-all use [...param], optional catch-all use [[...param]]
  • Route groups (name) organize files without changing URLs — multiple layouts at the same URL level
  • Parallel routes render multiple segments simultaneously using @slot convention
  • generateStaticParams pre-renders dynamic routes at build time for maximum performance
  • Middleware runs at the edge before routes execute — ideal for auth guards and redirects
  • The App Router uses React Server Components by default, reducing client-side JavaScript significantly

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading