Next.js App Router — Complete Guide to File-Based Routing
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/postsSpecial files at each level control behavior:
| File | Purpose |
|---|---|
page.tsx | Renders the route UI |
layout.tsx | Persistent wrapper, does not re-render |
loading.tsx | Suspense fallback shown while data loads |
error.tsx | Error boundary for the segment |
not-found.tsx | Rendered when notFound() is called |
route.ts | API 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 → /analyticsRoute 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 independentlyProgrammatic 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.tsxone level too high or low, causing wrong UI nesting - Using
useRouterfromnext/router(Pages Router) instead ofnext/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
Linkover programmatic navigation for SEO and prefetching - Use
generateStaticParamsfor content-heavy routes to pre-render at build time - Add
loading.tsxat every data-fetching level for instant visual feedback - Define
generateMetadataper page for dynamic, SEO-accuratetitleanddescriptiontags
Key Takeaways
- The App Router is stable since Next.js 14 and recommended for all new projects
page.tsxmakes a folder a public route;layout.tsxwraps 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
@slotconvention generateStaticParamspre-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