Next.js App Router Explained: File-Based Routing for Beginners

A clear, beginner-friendly walkthrough of the Next.js App Router. Learn how folders become URLs, how layouts wrap pages, and how dynamic routes, loading states, and error boundaries work in 2026.

Next.jsbeginner
12 min read

The single biggest "aha" moment in Next.js is realising that the entire router is just your folder structure. There is no <Routes> config, no React Router setup, no route table to maintain. You make a folder, drop a page.tsx inside, and that path is now a real URL with server rendering, code splitting, and prefetching already on.

This guide walks through the App Router from the ground up: how folders map to URLs, how layouts wrap pages, what the special files (page, layout, loading, error, not-found) actually do, how dynamic routes capture URL parameters, and how route groups let you organise without affecting URLs. It is the tour every new Next.js developer in 2026 needs.

The Core Rule: Folders Are URLs

Inside the app/ directory:

  • A folder name becomes a URL segment.
  • A page.tsx (or .jsx) file makes that folder visit-able.
  • A folder without a page.tsx is part of the structure but not a route on its own.

Concrete example:

CodeCode
app/
  page.tsx                  → /
  about/
    page.tsx                → /about
  blog/
    page.tsx                → /blog
    [slug]/
      page.tsx              → /blog/:slug

That is the whole routing system in three lines. Add folders, add URLs.

The Special Files Every App Router Project Uses

The App Router gives a small set of file names superpowers. Each one has to be inside a route folder.

  • page.tsx — the UI for that route. Required to make the folder a route.
  • layout.tsx — wraps page.tsx and any nested routes. Required at the root (app/layout.tsx).
  • loading.tsx — shown while the page or its data is loading (a Suspense fallback).
  • error.tsx — catches errors thrown inside the segment (a client component error boundary).
  • not-found.tsx — rendered when notFound() is called.
  • route.ts — turns the folder into an HTTP API endpoint instead of a page.

You can have all of them in the same folder. Next.js wires them together for you.

Layouts Nest Automatically

A layout.tsx wraps everything inside its folder — including child route segments. That makes shared chrome (sidebars, top navs, footers) trivial.

App.tsxApp.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="grid grid-cols-[16rem_1fr]">
      <Sidebar />
      <main>{children}</main>
    </div>
  );
}

Now every page under app/dashboard/* automatically renders inside that grid. The root app/layout.tsx wraps everything else and is where your <html> and <body> tags live.

Crucially, layouts persist across navigations. When you click from /dashboard/settings to /dashboard/billing, the dashboard layout does not unmount or refetch — only the inner page swaps. State inside the layout (an open menu, a search input) survives.

Dynamic Routes Use Square Brackets

To capture a part of the URL as a parameter, name the folder with brackets.

CodeCode
app/blog/[slug]/page.tsx           → /blog/anything-here
app/shop/[category]/[id]/page.tsx  → /shop/shoes/123
app/docs/[...path]/page.tsx        → catch-all: /docs/a/b/c

Inside the page, read params from props. In Next.js 15+ they are async:

App.tsxApp.tsx
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) notFound();
  return <article>{post.title}</article>;
}

Search-string params work the same way: searchParams: Promise<{ q?: string }>. Always await them — that change in Next.js 15 trips up everyone migrating from older versions.

Route Groups Organise Without Adding URLs

Wrap a folder name in parentheses to group routes without that name showing up in the URL.

CodeCode
app/
  (marketing)/
    layout.tsx     ← marketing-only nav + footer
    page.tsx       → /
    pricing/page.tsx → /pricing
  (app)/
    layout.tsx     ← logged-in chrome
    dashboard/page.tsx → /dashboard

(marketing) and (app) are invisible in URLs but each gets its own layout. This is the cleanest way to give your marketing pages and your authenticated app totally different chrome without any conditional rendering inside a single layout.

Loading and Error States, the Easy Way

Drop loading.tsx next to a page.tsx and Next.js automatically wraps the page in a Suspense boundary. While the server component is fetching, users see your loading UI instantly — no spinner code in the page itself.

App.tsxApp.tsx
// app/blog/loading.tsx
export default function Loading() { return <p>Loading posts…</p>; }

error.tsx works the same way for runtime errors. It must be a client component ("use client" at top) and gets error and reset props so you can show a friendly message and a "Try again" button.

Linking and Navigation

Use <Link> from next/link for internal navigation. It enables prefetching (the next page is fetched as the link enters the viewport) and client-side transitions (no full reload).

App.tsxApp.tsx
import Link from "next/link";
<Link href="/blog/hello-world">Read the post</Link>

For programmatic navigation inside event handlers, use the useRouter hook from next/navigation (note: from next/navigation, not next/router, which is the old pages-router import).

Common Mistakes Beginners Make

  • Forgetting page.tsx. A folder without one is not a route.
  • Importing useRouter from next/router. Use next/navigation in App Router code.
  • Not awaiting params or searchParams in Next.js 15+. They are promises now.
  • Putting <html> and <body> in a non-root layout. Only app/layout.tsx should contain them.
  • Using route groups to share URL prefixes. Use a normal folder for that; route groups are for shared layouts only.

Quick Reference

  • app/foo/page.tsx/foo
  • app/foo/[id]/page.tsx/foo/:id (async params)
  • app/foo/[...slug]/page.tsx → catch-all
  • app/(group)/foo/page.tsx/foo (group is invisible)
  • layout.tsx wraps the segment + children, persists across nav
  • loading.tsx = Suspense fallback. error.tsx = error boundary (client component)
  • not-found.tsx rendered when you call notFound() from next/navigation
  • route.ts turns the folder into an API endpoint; export GET, POST, etc.
  • Navigate with <Link> (declarative) or useRouter() from next/navigation (programmatic)
Rune AI

Rune AI

Key Insights

  • Folders inside app/ are URL segments; page.tsx makes them routable.
  • layout.tsx wraps a segment and persists across navigations.
  • [param] captures dynamic segments; params is async in Next.js 15+ — always await.
  • loading.tsx and error.tsx give you Suspense fallbacks and error boundaries for free.
  • Route groups (name) share layouts without showing up in URLs.
RunePowered by Rune AI

Frequently Asked Questions

App Router or Pages Router for new projects?

pp Router. It is the default since Next.js 15 and where all new framework features land. The Pages Router is supported for legacy apps but no new code should target it.

Are layouts re-rendered on every navigation?

No. Layouts persist across navigations within the same segment, which keeps state (open dropdowns, scroll position in sidebars) intact.

Can a folder have both a `page.tsx` and a `route.ts`?

No. A segment is either a page or an HTTP endpoint, not both.

How do I share state between routes?

For UI state, lift it into a layout (it persists). For server data, the App Router caches `fetch` automatically. For client state across pages, use React Context inside a client component placed in a layout.

Why is my `params` an empty object?

You probably forgot to `await` it in Next.js 15+. The shape is `params: Promise<{ slug: string }>` now.

Conclusion

The App Router turns navigation into a filesystem operation. Folders become URLs, special files give those folders behaviour, and layouts let you compose chrome without prop-drilling. Spend an afternoon building a small site with three or four pages, a dynamic blog route, and a shared dashboard layout — the model will lock in fast.