App Router

The App Router is Next.js’s server-first routing system. Every component is a Server Component by default. You opt into client-side interactivity explicitly with "use client".

Mental Model

The App Router treats your app/ directory as a component tree, not a page tree. Each folder is a route segment, and special files control what renders at that segment. Layouts persist across navigations. Pages are the leaf content.

app/
  layout.tsx        # Root layout (wraps everything)
  page.tsx          # Home page (/)
  blog/
    layout.tsx      # Blog layout (persists across blog pages)
    page.tsx        # Blog index (/blog)
    [slug]/
      page.tsx      # Blog post (/blog/my-post)
      loading.tsx   # Loading skeleton for this route
      error.tsx     # Error boundary for this route

File Conventions

FilePurposeRequired?
page.tsxUI for a route, makes segment publicly accessibleYes (to create a route)
layout.tsxShared UI that wraps children, persists across navigationsYes (root only)
loading.tsxInstant loading skeleton via React SuspenseNo
error.tsxError boundary, catches errors in the segment and belowNo
not-found.tsxCustom 404 for a segmentNo
route.tsAPI endpoint (GET, POST, etc.) - cannot coexist with page.tsxNo
template.tsxLike layout but re-mounts on navigation (new instance per nav)No
default.tsxFallback for parallel routes when no matchNo

Root Layout

Every app needs a root layout. It must include <html> and <body> tags.

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Gotcha: The root layout replaces _app.tsx and _document.tsx from the Pages Router. There are no _app or _document files in App Router projects.

Nested Layouts

Layouts compose. A blog layout wraps all blog pages while the root layout wraps everything.

// app/blog/layout.tsx
export default function BlogLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="blog-container">
      <nav>Blog Navigation</nav>
      <main>{children}</main>
    </div>
  );
}

The key property: layouts don’t re-render when you navigate between sibling pages. State in the blog layout persists as you move between /blog/post-1 and /blog/post-2.

Route Groups

Use parentheses to organize routes without affecting the URL structure:

app/
  (marketing)/
    about/page.tsx        # /about
    pricing/page.tsx      # /pricing
    layout.tsx            # Shared marketing layout
  (app)/
    dashboard/page.tsx    # /dashboard
    settings/page.tsx     # /settings
    layout.tsx            # Shared app layout

The (marketing) and (app) folders don’t create URL segments. They let you apply different layouts to different sections.

Dynamic Routes

// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);

  return <article>{post.content}</article>;
}

Gotcha: In Next.js 15+, params is a Promise. You must await it. This was a breaking change from v14 where params was a plain object.

Catch-all and Optional Catch-all

app/docs/[...slug]/page.tsx         # /docs/a, /docs/a/b, /docs/a/b/c
app/docs/[[...slug]]/page.tsx       # same as above + /docs (index)
// app/docs/[...slug]/page.tsx
export default async function Docs({ params }: { params: Promise<{ slug: string[] }> }) {
  const { slug } = await params;
  // slug = ["a", "b"] for /docs/a/b
}

Parallel Routes

Render multiple pages in the same layout simultaneously using named slots:

app/
  @analytics/page.tsx
  @team/page.tsx
  layout.tsx
  page.tsx
// app/layout.tsx
export default function Layout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <>
      {children}
      {analytics}
      {team}
    </>
  );
}

API Routes (Route Handlers)

// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET() {
  const posts = await getPosts();
  return NextResponse.json(posts);
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const post = await createPost(body);
  return NextResponse.json(post, { status: 201 });
}

Tip: For mutations from your own UI, prefer Server Actions over API routes. API routes are best for webhooks, third-party integrations, and public APIs.

Static vs Dynamic

Routes are static by default. Next.js makes a route dynamic when you use:

  • cookies() or headers()
  • searchParams in a page
  • connection() or unstable_noStore()
  • A fetch call with cache: "no-store"

You can force static generation with generateStaticParams:

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

Next: Server Components | Caching