Skip to main content
PProfileMatch

Next.js App Router and File-Based Routing Explained

Web DevelopmentLesson 1 of 45 min read

What you'll learn

  • Map a folder tree under app/ to the URLs it produces
  • Write a root layout, a nested layout and a dynamic route that reads params
  • Use loading, error and not-found files to handle each state of a route
  • Navigate between pages with Link and avoid the common routing errors

On this page

  1. Folders are segments, page.tsx makes them public
  2. The root layout and the first page
  3. Dynamic routes, params and not-found
  4. Files for every state of a route
  5. Common mistakes
  6. How this shows up in real projects and interviews

A neighbourhood tool library wants a website: a home page, a list of tools, and a page for each tool. In a plain React single-page app you would install a router, write a route table and keep it in sync with your files. Next.js removes that step. You create folders and special files inside an app/ directory, and the folder names become the URLs. The structure of your project is the route table.

Folders are segments, page.tsx makes them public

Each folder inside app/ is a URL segment, but a folder only becomes a reachable page when it contains a file named page.tsx (or .js, .jsx, .ts). That means you can keep helper files such as components next to your routes without accidentally publishing them. Here is the tool library laid out on disk, with the URL each file creates.

Project tree and the URLs it creates
app/
  layout.tsx                -> wraps every page
  page.tsx                  -> /
  not-found.tsx             -> any unmatched URL
  (info)/
    about/
      page.tsx              -> /about
  tools/
    layout.tsx              -> wraps /tools and below
    loading.tsx             -> shown while /tools loads
    error.tsx               -> shown if /tools throws
    page.tsx                -> /tools
    [toolId]/
      page.tsx              -> /tools/drill, /tools/ladder
  api/
    health/
      route.ts              -> GET /api/health (JSON)

Three details are worth noticing. Parentheses make a route group: (info) organises files without appearing in the URL, so /about not /info/about. Square brackets make a dynamic segment: [toolId] matches any single value and hands it to your code as a parameter. And a route.ts file creates an HTTP endpoint instead of a page, so a folder cannot contain both a page and a route at the same level.

The root layout and the first page

Every App Router project needs a root layout at app/layout.tsx. It must render the html and body tags, because Next.js does not add them for you. A layout receives the current page as children, and it stays mounted when you navigate between pages beneath it, so its state and DOM are preserved.

app/layout.tsx
import type { Metadata } from "next";
import Link from "next/link";
import type { ReactNode } from "react";

export const metadata: Metadata = {
  title: { default: "Toolshed", template: "%s | Toolshed" },
  description: "Borrow tools from neighbours instead of buying them.",
};

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <nav>
          <Link href="/">Home</Link> <Link href="/tools">Tools</Link> <Link href="/about">About</Link>
        </nav>
        {children}
      </body>
    </html>
  );
}
app/page.tsx
import Link from "next/link";

export default function HomePage() {
  return (
    <main>
      <h1>Borrow, do not buy</h1>
      <p>Forty-two neighbours share drills, ladders and a tile cutter.</p>
      <Link href="/tools">Browse the shelf</Link>
    </main>
  );
}

Visiting / renders the navigation bar from the layout with three links on one line, followed by the heading, the sentence and a link. Clicking "Tools" does not reload the whole document: Link fetches what is needed for the new page and swaps it in, and the nav stays exactly where it is. The metadata export sets the tab title to "Toolshed" and, thanks to the template, child pages that export a title of "Drill" will show "Drill | Toolshed".

Dynamic routes, params and not-found

A page inside a dynamic folder receives the segment values through a params prop. In Next.js 15 and later, params is a Promise that you await; in earlier versions it was a plain object. Check the version in your package.json, because the type you write differs, and the examples here use the newer form. Missing records should call notFound(), which renders the nearest not-found.tsx and sends a 404 status.

lib/tools.ts and app/tools/[toolId]/page.tsx
// lib/tools.ts
export type Tool = { id: string; name: string; deposit: number };

const TOOLS: Tool[] = [
  { id: "drill", name: "Cordless drill", deposit: 20 },
  { id: "ladder", name: "Three-step ladder", deposit: 10 },
  { id: "tile-cutter", name: "Tile cutter", deposit: 35 },
];

export function listTools(): Tool[] {
  return TOOLS;
}

export function findTool(id: string): Tool | undefined {
  return TOOLS.find((tool) => tool.id === id);
}

// app/tools/[toolId]/page.tsx
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { findTool } from "@/lib/tools";

type Props = { params: Promise<{ toolId: string }> };

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { toolId } = await params;
  const tool = findTool(toolId);
  return { title: tool ? tool.name : "Tool not found" };
}

export default async function ToolPage({ params }: Props) {
  const { toolId } = await params;
  const tool = findTool(toolId);
  if (!tool) notFound();

  return (
    <article>
      <h1>{tool.name}</h1>
      <p>Refundable deposit: {tool.deposit} pounds.</p>
    </article>
  );
}

Going to /tools/ladder shows a heading "Three-step ladder" with the deposit line beneath it and the tab title "Three-step ladder | Toolshed". Going to /tools/chainsaw finds no record, so notFound() stops rendering and the not-found page appears with a 404 status. The @/ import alias is configured by the project template in tsconfig.json; if you chose not to enable it, use relative imports.

Files for every state of a route

FilePurposeNotes
page.tsxThe UI for a URLMakes the folder publicly routable
layout.tsxShared UI around child routesPreserved across navigation; root layout is required
loading.tsxFallback shown while the segment's data loadsWraps the page in a Suspense boundary for you
error.tsxFallback when rendering throwsMust be a Client Component ("use client")
not-found.tsxUI for notFound() and unmatched URLsA root-level one handles URLs that match nothing
route.tsAn HTTP handler (GET, POST...)Cannot share a segment with page.tsx
app/tools/error.tsx
"use client";

export default function ToolsError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <section>
      <h2>The shelf could not be loaded</h2>
      <p>Please try again. Reference: {error.digest ?? "none"}</p>
      <button type="button" onClick={() => reset()}>Try again</button>
    </section>
  );
}

Common mistakes

  • Creating a folder without `page.tsx`. The URL returns 404 because nothing is public until a page file exists.
  • A root layout without `html` and `body`. Next.js reports an error that the root layout is missing the required tags, and the page does not render.
  • Using `<a href>` for internal links. It works but triggers a full page load and loses the layout state; use Link from next/link.
  • Forgetting `"use client"` in `error.tsx`. The build fails, since error boundaries need client-side behaviour and event handlers.
  • Accessing `params.toolId` directly in newer versions. Next.js 15 logs a warning that params should be awaited before its properties are used; in later releases it is an error.
  • Wrong folder casing. app/Tools/page.tsx creates /Tools, not /tools, and URL matching is case-sensitive on most hosts.

How this shows up in real projects and interviews

In a code review you will mostly be asked to place things correctly: shared chrome in a layout, a per-page title in metadata, empty states in not-found, and loading UI in loading.tsx. Interviewers for React roles that use Next.js often ask how routing differs from React Router and what a layout is. A good answer says routes come from the file system, layouts persist across navigations, and special files provide loading, error and not-found states per segment. Older projects may still use the pages/ directory; both routers can coexist during migration, but new work generally targets app/.

Try it yourself

Create a new Next.js app and build a small recipe site: a home page, /recipes listing three recipes, and /recipes/[slug] that shows a recipe or a custom not-found page. Add a recipes layout with a sidebar, a loading.tsx that you can see by adding an await new Promise(r => setTimeout(r, 1500)) in the list page, and a route group called (legal) containing /privacy.

Show a hint

Run npm run dev and check that (legal) does not appear in the URL. To test not-found, visit a slug that is not in your data and confirm the 404 status in the Network tab.

Key takeaways

  • Folders under app/ become URL segments, and a page.tsx file is what makes a segment reachable.
  • The root layout is required and must render html and body; nested layouts persist across navigations.
  • Dynamic segments use [name], route groups use (name), and in Next.js 15 and later params is awaited.
  • loading.tsx, error.tsx (client component) and not-found.tsx give each route its own loading, failure and missing states.

Test yourself

A trick from this lesson's ride: A folder is a street; only a page.tsx opens it to visitors. Parentheses are invisible. Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

How does routing work in the Next.js App Router?

Each folder inside app/ is a URL segment, and a page.tsx file inside it makes that segment a public route. Square brackets create dynamic segments, parentheses create route groups that are not part of the URL, and layout.tsx wraps the pages below it.

What is the difference between layout.tsx and page.tsx in Next.js?

A page.tsx is the unique UI of one URL. A layout.tsx wraps all pages in its folder and below, receives them as children, and stays mounted when you navigate between them. Use layouts for shared navigation and chrome, pages for the content that changes.

Why do I get a 404 in Next.js for a folder I created?

A folder alone is not a route. It needs a page.tsx (or route.ts) file with a default export, and the folder name must match the URL exactly, including case. Also check that you are not inside a private folder starting with an underscore, which is excluded from routing.