Mastering the Next.js App Router
The App Router in Next.js is a ground-up rethink of routing, layouts, and data fetching. If you're coming from the Pages Router, some conventions will feel familiar, but the underlying model is fundamentally different.
File-Based Routing, Evolved
The App Router uses a app/ directory where each folder becomes a route segment. Special files control what renders:
app/
├── layout.tsx # Root layout (wraps everything)
├── page.tsx # Home page (/)
├── posts/
│ ├── layout.tsx # Posts layout (wraps all /posts/* pages)
│ ├── page.tsx # Posts index (/posts)
│ └── [slug]/
│ └── page.tsx # Single post (/posts/my-post)
└── tags/
├── page.tsx # All tags (/tags)
└── [name]/
└── page.tsx # Tag page (/tags/react)
Layouts are the big innovation. They persist across navigations — the root layout stays mounted when you navigate between /posts and /tags. This means shared UI (header, sidebar, footer) never re-renders.
Async Components by Default
In the App Router, page components can be async:
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await db.query.posts.findFirst({
where: eq(posts.slug, slug),
});
if (!post) notFound();
return <Article post={post} />;
}
No getServerSideProps. No getStaticProps. The component itself is the data-fetching layer. Next.js automatically deduplicates fetch calls across components that render in the same request.
Metadata API
SEO metadata moves from <Head> to an exported generateMetadata function:
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.headerImageUrl],
},
};
}
This runs on the server and produces the right <meta> tags without any client JavaScript.
Route Groups and Parallel Routes
Route groups (groupName) let you organize routes without affecting the URL:
app/
├── (marketing)/
│ ├── layout.tsx # Marketing layout
│ ├── page.tsx # /
│ └── about/
│ └── page.tsx # /about
└── (dashboard)/
├── layout.tsx # Dashboard layout (with sidebar)
└── settings/
└── page.tsx # /settings
Same URL space, different layouts. Parallel routes (@slot) take this further, rendering multiple page components in the same layout simultaneously.
Streaming and Loading States
Every route segment can have a loading.tsx that shows instantly while the page component awaits data:
// app/posts/loading.tsx
export default function Loading() {
return <PostListSkeleton />;
}
Under the hood, Next.js wraps your page in a <Suspense> boundary with this loading component as the fallback. The result: instant navigation with progressive content streaming.
Migration Tips
-
Move pages one at a time — the Pages and App Router coexist
-
Replace
getServerSidePropswith directawaitin components -
Move global styles and providers to the root
layout.tsx -
Use
"use client"only for components that genuinely need browser APIs
The App Router is opinionated, but those opinions align well with modern React patterns. Once you internalize the layout/page/loading model, building full-stack React applications feels remarkably streamlined.