Nathaniel's blog
Back to posts

Mastering the Next.js App Router

Nathaniel LinJanuary 25, 20269 min read0 views
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 getServerSideProps with direct await in 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.

Share this post

Reactions