Skip to main content
ANVISoftware Solutions
Lesson 12 of 18Intermediate16 min

Routing and Layouts

By the end of this lesson

Use file-based routing and share layout across pages.

React on its own has no idea what a URL is. It renders components; how the browser address bar relates to them is somebody else's job. In Next.js that job is done by the file system: the folders you create under app are the segments of your URLs, and files with particular names give each segment its page, its surrounding layout, and what to show while it loads or when it fails.

This means routing is configuration you can see. There is no list of routes to keep in step with the components — moving a folder moves the URL.

The directory structure for an employee directory and a course catalogue
Text
app/
  layout.tsx                     the shell around everything: /
  page.tsx                       /
  employees/
    layout.tsx                   wraps every URL under /employees
    page.tsx                     /employees
    loading.tsx                  shown while /employees is loading
    error.tsx                    shown if /employees throws
    [id]/
      page.tsx                   /employees/e-104
      edit/
        page.tsx                 /employees/e-104/edit
  courses/
    page.tsx                     /courses
    [slug]/
      page.tsx                   /courses/react-nextjs
      lessons/
        [lessonSlug]/
          page.tsx               /courses/react-nextjs/lessons/rn-jsx
    not-found.tsx                shown when a course slug does not exist
  • A folder is a URL segment. A page.tsx inside it makes that URL reachable — a folder without one is just a container, useful for grouping files that are not routes.
  • Square brackets mark a dynamic segment. [id] matches any single segment and hands you its value, so one file serves every employee.
  • Nesting continues as deep as you need. The lesson page is four segments down and is still one file in one folder.
  • loading, error and not-found apply to the segment they sit in and everything below it, unless a deeper folder provides its own.
  • Only these reserved filenames create behaviour. Any other file in the folder — a component, a helper, a test — is ignored by the router, so you can keep route-specific code next to the route that uses it.
app/employees/layout.tsx — shared around every employee page
TSX
import type { ReactNode } from "react";
import Link from "next/link";
import { getDepartments } from "@/data/employees";

export default async function EmployeesLayout({ children }: { children: ReactNode }) {
  const departments = await getDepartments();

  return (
    <div className="grid gap-8 lg:grid-cols-[220px_1fr]">
      <nav aria-label="Departments">
        <ul>
          {departments.map((department) => (
            <li key={department.slug}>
              <Link href={"/employees?department=" + department.slug}>
                {department.name}
              </Link>
            </li>
          ))}
        </ul>
      </nav>

      {/* The page for the current URL renders here. */}
      <main>{children}</main>
    </div>
  );
}
  • A layout receives the matching page as children and wraps it. Nested layouts stack: the root layout wraps this one, which wraps the page.
  • The departments are loaded once in the layout rather than in every page underneath it, because this is a server component and it can await data directly.
  • aria-label on the nav names the region, which matters as soon as a page has more than one navigation landmark. Without it a screen reader user hears "navigation" twice and cannot tell them apart.
  • Link performs a client-side navigation: it swaps the page content without a full document reload, and prefetches the target when the link is near the viewport. A plain anchor works but reloads everything, which throws away the whole benefit.
  • The root layout is the only one that renders html and body tags. Every other layout returns ordinary markup.
app/employees/[id]/page.tsx — reading a dynamic segment
TSX
import { notFound } from "next/navigation";
import { getEmployeeById } from "@/data/employees";

export default async function EmployeePage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const employee = await getEmployeeById(id);

  if (!employee) notFound();

  return (
    <article>
      <h1>{employee.name}</h1>
      <p>{employee.jobTitle} · {employee.department}</p>
    </article>
  );
}
  • params carries the dynamic segments for this route. In current Next.js versions it is a promise, so you await it — the same applies to searchParams.
  • The key of the object matches the folder name. A folder called [id] gives you params.id; renaming the folder renames the property.
  • notFound() stops rendering and shows the nearest not-found file. Returning your own "not found" markup instead would send a 200 status for a page that does not exist, which misleads search engines and monitoring.
  • An unknown id is an expected outcome of a URL someone typed or bookmarked, not an exceptional failure. Handling it explicitly is part of the route's job.

The reserved files, and what each one is for:

page.tsx
Makes the URL reachable and renders its content. Without one, the folder contributes a segment but has no page of its own.
layout.tsx
Wraps the page and everything nested below it. Receives children. Persists across navigation within its segment.
loading.tsx
Shown automatically while the segment's data is being awaited. It saves you writing a loading state by hand for server-rendered data.
error.tsx
Shown when rendering in that segment throws. It is a client component and receives a reset function so the user can retry without reloading the page.
not-found.tsx
Shown when notFound() is called in that segment, and for URLs that match no route at all.
A folder in brackets, such as (marketing)
A route group. It organises files and can add a layout without adding a URL segment — useful when two sections of a site need different shells.
template.tsx
Like a layout, but a fresh instance on every navigation. Reach for it in the uncommon case where you need the wrapper's state reset as the user moves between pages.

Summary

  • Folders under app are URL segments; reserved filenames give a segment its page and its surroundings
  • page makes a URL reachable, layout wraps it and everything below, loading and error cover the states around it
  • Square brackets mark a dynamic segment, and params arrives as a promise you await
  • Layouts persist across navigation within their segment, so their state survives and their data does not re-fetch
  • Link navigates on the client and prefetches; a plain anchor reloads the document

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Try it yourself

Sketch the folders and files for this: a courses listing at /courses, a course page at /courses/react-nextjs, a lesson page at /courses/react-nextjs/lessons/rn-jsx, a sidebar of modules shared by the course page and every lesson page, and a friendly page for a course slug that does not exist.

Then decide which segment should hold the loading file, and why not the root.

Show solution

The sidebar goes in a layout at app/courses/[slug]/layout.tsx. That is the deepest segment shared by the course page and its lessons, so the sidebar persists as the user moves between lessons — and its data is fetched once rather than per lesson.

not-found.tsx sits in app/courses/[slug]/, so the message can be specific: this course does not exist, here is the catalogue. A root-level not-found would be the generic fallback for every unmatched URL.

The loading file belongs in the segment whose data is slow, usually app/courses/[slug]/. At the root it would replace the entire page including the navigation, so every navigation would blank the whole screen instead of just the part that is actually waiting.

There is a judgement call about the lessons folder. It has no page of its own, because /courses/react-nextjs/lessons is not a page anyone needs — it is a container segment. Adding a page there would mean deciding what that URL should show, and "redirect to the first lesson" is a reasonable answer if you want it to be reachable.

Text
app/
  courses/
    page.tsx                       /courses
    [slug]/
      layout.tsx                   module sidebar, shared by the course and its lessons
      page.tsx                     /courses/react-nextjs
      loading.tsx                  shown while course data is awaited
      not-found.tsx                unknown course slug
      lessons/
        [lessonSlug]/
          page.tsx                 /courses/react-nextjs/lessons/rn-jsx

Think about it

Think about it

An employee page shows a header with the employee's name, and tabs for Profile, Team and History, each its own URL.

Should the header live in a layout or be repeated in each of the three pages? What breaks with each choice?

Show solution

A layout at app/employees/[id]/ is the better answer. The employee is the same across all three tabs, so the layout fetches the employee once, the header does not flicker as the user switches tabs, and the active tab styling has a single home.

Repeating it in each page means three fetches of the same employee and three copies of the header markup, so a change to the header is three edits and the risk that one is forgotten.

What the layout choice costs: if the header needs to show something that differs per tab — a count that only the History view knows — the layout cannot see it, because the layout does not re-run as you navigate between its pages. Threading that upward is awkward, and the usual answers are to fetch it in the layout too, or to render that part in the page rather than the header.

This is the general shape of the decision. A layout gives you persistence and one fetch; a page gives you access to per-page data. Choose by asking what changes with the URL.

Saved in this browser only.