Project Structure, Pages, Layouts, and Templates
In a Next.js App Router project, the app directory is not just a place to store screens. It is a route tree. Folders define URL segments, and special files such as page.tsx, layout.tsx, template.tsx, loading.tsx, error.tsx, and not-found.tsx tell Next.js how to render each segment. The outcome of this lesson is practical: you should be able to look at an app folder, predict the URLs it creates, decide where shared UI belongs, and know when a template is needed instead of a layout.
This chapter connects directly to App Router foundations because routing is the spine that later features attach to. Server Components, nested navigation, metadata, streaming, error recovery, route handlers, and data fetching all become easier once you understand how the filesystem becomes a render tree.
How the App Router Reads a Project
Next.js walks the app directory and builds a tree from route segments. A normal folder name becomes part of the URL. A folder named dashboard contributes /dashboard. A dynamic folder such as [teamId] matches a URL parameter. A route group such as (marketing) organizes files without adding a URL segment. A private folder such as _components is ignored by routing and is useful for colocated helpers.
The special file page.tsx makes a segment publicly reachable. Without a page.tsx, a folder can still contribute layout structure to child routes, but it is not itself a page. A layout.tsx wraps the page and every child segment below it. Layouts persist across navigation when the user moves between routes that share the same layout, so stateful client components inside the layout can keep their state. A template.tsx also wraps children, but it gets a fresh instance on navigation. That makes templates useful for per-page enter animations, resetting client form state, or running effects again when moving between sibling routes.
Rendering is nested from the root downward. The root layout must return html and body. Child layouts return UI wrappers around children. The final leaf page supplies the route content. Conceptually, a request for /dashboard/settings renders the root layout, then the dashboard layout, then the settings page. If a template exists at one of those levels, it is inserted at that level and remounted for matching navigations.
Syntax and File Anatomy
A small App Router project can start with this shape:
app/
layout.tsx
page.tsx
dashboard/
layout.tsx
page.tsx
settings/
page.tsx
blog/
[slug]/
page.tsx
(marketing)/
pricing/
page.tsx
_components/
SiteNav.tsx
This tree creates /, /dashboard, /dashboard/settings, /blog/:slug, and /pricing. The (marketing) folder is not visible in the URL, and _components creates no route.
The root layout is the outer document shell:
import "./globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
The important detail is not the syntax alone. This component wraps every route in the app. Put global providers, document structure, and site-wide CSS here. Do not put route-specific data or section-specific navigation here unless every route truly needs it.
Example 1: A Home Page
The smallest reachable route is a segment with a page file. At the root, app/page.tsx maps to /:
export default function HomePage() {
return (
<main>
<h1>Course Dashboard</h1>
<p>Choose a lesson to continue.</p>
</main>
);
}
When the browser requests /, Next.js renders RootLayout and places HomePage where the root layout renders {children}. The deterministic output is an HTML document whose body contains the heading Course Dashboard and the paragraph Choose a lesson to continue.
Example 2: Shared Section UI with a Layout
Layouts are the right tool when multiple pages need the same surrounding UI and that UI should persist while navigating within the section.
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<section>
<nav aria-label="Dashboard">
<a href="/dashboard">Overview</a>
<a href="/dashboard/settings">Settings</a>
</nav>
<div>{children}</div>
</section>
);
}
Place this in app/dashboard/layout.tsx. It wraps both /dashboard and /dashboard/settings. If the navigation contains a client-side expanded menu or search input, that client state can persist when moving between those two routes because the layout instance is shared. That persistence is usually desirable for shells, sidebars, tabs, and authenticated app frames.
Example 3: Dynamic Segments
Dynamic folders capture URL parameters. A file at app/blog/[slug]/page.tsx receives a params object:
type BlogPageProps = {
params: Promise<{ slug: string }>;
};
export default async function BlogPage({ params }: BlogPageProps) {
const { slug } = await params;
return (
<main>
<h1>Post: {slug}</h1>
</main>
);
}
A request for /blog/routing-basics produces a page with the heading Post: routing-basics. The folder name [slug] is the contract between the route tree and the component. Use descriptive parameter names such as [courseSlug] or [lessonId] when the route contains multiple identifiers.
Example 4: Resetting State with a Template
A template has the same signature as a layout, but its lifecycle is different. Use it when each navigation should create a new wrapper instance.
export default function LessonTemplate({ children }: { children: React.ReactNode }) {
return <div className="lesson-frame">{children}</div>;
}
If this file is placed at app/dashboard/settings/template.tsx, the template wraps the settings page and remounts when the user navigates into that segment. This is useful when a page-local client component needs a fresh initial state. It is the wrong choice for persistent navigation or expensive shared UI because remounting discards state and repeats setup work.
Design Choices and Trade-Offs
Prefer shallow route trees until a product concept actually needs nesting. Deep nesting can accurately model a complex interface, but it also spreads rendering behavior across many files. A useful rule is to create a folder when it represents a URL segment, a shared layout boundary, or a meaningful organizational group. Avoid creating folders only because a component feels large.
Choose layouts for stable shells. Choose templates for deliberate resets. Choose route groups to separate concerns such as (marketing) and (app) without changing public URLs. Use private folders for colocated components that should never become routes. Colocation is one of the App Router’s strengths: a route can keep its page, loading UI, error UI, and local components nearby, which makes the behavior easier to audit.
There is also a performance trade-off. Persistent layouts can avoid repeated work and preserve client state, but anything placed high in the tree affects many routes. A large client provider in the root layout can increase JavaScript cost across the entire site. Keep Server Components as the default, and move interactivity into small Client Components only where browser state or event handlers are required.
Failure Modes and Troubleshooting
- Symptom: a folder does not create a URL. Cause: the segment is missing
page.tsx, or it is a route group/private folder. Diagnose: inspect the exact path underappand confirm the special file name. Correct: addpage.tsxat the segment that should be reachable. - Symptom: a sidebar loses state when navigating. Cause: the sidebar is inside a page or template instead of a shared layout. Diagnose: find the nearest common parent segment for the affected routes. Correct: move the persistent shell into that parent
layout.tsx. - Symptom: a form keeps stale values when moving between sibling pages. Cause: the form is inside a persistent layout. Diagnose: check whether the component is mounted above the changing page segment. Correct: move it into the page or introduce a
template.tsxat the segment that should reset. - Symptom: the URL includes an unwanted organizational word. Cause: a normal folder was used instead of a route group. Diagnose: compare the folder name with the expected URL. Correct: rename organizational folders with parentheses, such as
(marketing).
Security, Performance, and Reliability
Project structure affects more than navigation. Keeping sensitive data access in Server Components prevents that implementation from being shipped to the browser. Keeping broad providers out of the root layout reduces bundle size and limits accidental exposure of client-only state. Segment-level error and loading files improve reliability because failures and slow data dependencies can be handled close to the route that owns them.
Route groups can also separate public and authenticated areas without changing URLs. For example, (public) and (app) can each have different layouts. That organization makes it harder to accidentally show authenticated navigation on a public page, but it does not replace authorization checks in data access code.
Hands-On Lab
Prerequisites: a working Next.js App Router project, Node.js installed, and a terminal in the project root.
- Create
app/layout.tsxwith the root layout shown above. - Create
app/page.tsxwith the home page example. - Create
app/dashboard/layout.tsxwith the dashboard navigation example. - Create
app/dashboard/page.tsxthat returns anh1withDashboard Overview. - Create
app/dashboard/settings/page.tsxthat returns anh1withSettings. - Create
app/blog/[slug]/page.tsxwith the dynamic segment example. - Run the development server with
npm run dev.
Verification: visit /, /dashboard, /dashboard/settings, and /blog/routing-basics. Confirm that the dashboard navigation appears on both dashboard routes but not on the home or blog routes. Confirm that the blog page prints the slug from the URL.
Cleanup: remove the lab folders or commit them on a feature branch. If you changed an existing root layout, restore its previous providers, metadata, and global CSS imports before continuing.
Assessment Exercises
- A product has
/account/profileand/account/billing. Where should the account navigation live, and why? - You need
/pricingand/contactto share a marketing layout without adding/marketingto the URL. Sketch the folder structure. - A search box in a layout keeps its value when users move between sibling pages, but the product owner wants it cleared every time. Which file type changes that behavior?
- Given
app/courses/[courseSlug]/lessons/[lessonSlug]/page.tsx, what parameters should the page receive for/courses/nextjs/lessons/routing? - Identify one thing that belongs in the root layout and one thing that should usually stay in a nested layout.
Summary
The App Router turns folders into route segments and special files into rendering behavior. A page.tsx makes a route reachable, a layout.tsx creates persistent shared UI, and a template.tsx creates a wrapper that remounts during navigation. Good project structure makes URLs predictable, keeps shared UI at the right level, limits unnecessary client JavaScript, and gives each route a clear place for loading, error, and not-found behavior.
