Skip to content
JL

Home

About

Blog

Contact

Shop

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. Next.js App Router Mental Model

Table of contents

  • Share on X
  • Discuss on X

Related Articles

Server and Client Components in Next.js
Next.js
11m
Sep 24, 2026

Server and Client Components in Next.js

Server and Client Components are a design boundary, not a contest between two rendering styles. Learn what crosses the boundary, how imports affect bundles, and how to keep interactive islands focused.

#Next.js#Server Components+4
Next.js Layouts, Templates, and Route Groups
Next.js
11m
Oct 1, 2026

Next.js Layouts, Templates, and Route Groups

Layouts, templates, and route groups shape navigation. Learn the persistence, reset, and URL semantics that make route structure intentional.

#Next.js#App Router+5
Our top five web development technologies of 2024
Web Development
15m
Jun 28, 2024

Our top five web development technologies of 2024

The web development landscape evolves rapidly, with new technologies emerging while established frameworks adapt to changing needs. Based on our experience building solutions for clients across various industries, these five technologies have proven most valuable for delivering robust, scalable applications in 2024.

#React#Vue.js+8
Ask me anything! 💬

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2026 • All rights reserved

Contents

  • The Route Is a Composition
  • Start on the Server
  • Use Client Components for Interaction
  • Layouts Are Durable Boundaries
  • A Product Page Walkthrough
  • Common Boundary Mistakes
  • A Practical Decision Process
  • What Is Next
  • Key Takeaways
Next.js

Next.js App Router Mental Model

September 17, 2026•5 min read
Joshua R. Lehman
Joshua R. Lehman
Author
Layered App Router architecture with route panels, a server data layer, and an interactive client component
Next.js App Router Mental Model

The App Router is not just a new folder convention. It is a rendering model: a route is assembled from server-rendered structure, nested UI boundaries, streamed data, and small pieces of browser interactivity. Once that model clicks, many Next.js decisions stop feeling like framework trivia.

The important shift is this: a component is not automatically code that runs in the browser. In an App Router application, components render on the server by default. You choose the client boundary only when the browser needs to own state, events, or an API that exists only in the browser.

The Route Is a Composition

Consider a product route:

app/
  products/
    [slug]/
      page.tsx
      loading.tsx
      error.tsx
  layout.tsx

When a visitor opens /products/walnut-desk, Next.js does not look for one giant page component and send it all to the browser. It composes the root layout, any nested layouts, the route page, and the matching loading or error boundary. The result can stream to the browser as work completes.

That composition is why colocating route-specific files matters. A loading.tsx is not a convention for its own sake; it is the pending UI for that segment. An error.tsx is the recovery boundary for that segment. A layout.tsx is the durable shell that can persist while child routes change.

Start on the Server

Server Components are the default because most page work is not interactive. Fetching a product, reading a database, checking server-only configuration, and rendering a heading do not require JavaScript to be shipped for that component.

// app/products/[slug]/page.tsx
import { notFound } from "next/navigation";
 
type ProductPageProps = {
  params: Promise<{ slug: string }>;
};
 
export default async function ProductPage({ params }: ProductPageProps) {
  const { slug } = await params;
  const product = await getProductBySlug(slug);
 
  if (!product) notFound();
 
  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <AddToCart productId={product.id} />
    </article>
  );
}

The page can access server-side resources. It can await data. It can render a client component such as AddToCart. What it cannot do is attach an onClick handler itself, because that would require the page component to become part of the client bundle.

Server Is the Default, Not a Limitation

Keeping a component on the server is often the simplest and fastest choice. Move to the client only at the smallest component that needs browser state or an event handler.

Use Client Components for Interaction

Add the "use client" directive at the top of a file only when the component needs hooks, event handlers, browser APIs, or client-side libraries. The directive creates a boundary: imports below it join the client bundle, while a Server Component can still render the client component and pass serializable props.

Choose the rendering boundary

Toggle the two component types to see the responsibility each side should own. This visual simulates the boundary; your real route still runs through Next.js.

Choose where this work belongs:

Server Components can read data and secrets before sending a rendered result to the browser. They cannot attach browser event handlers.

export default async function Page() {
  const product = await getProduct();
  return <AddToCart productId={product.id} />;
}

The most common mistake is adding "use client" high in the tree to make one button work. That turns an entire layout or page into browser JavaScript and blocks server-only imports. Keep the interactive island small: pass it the data it needs, then let it manage only the interaction it owns.

Layouts Are Durable Boundaries

Layouts answer a different question from pages. A page owns the UI for a specific route. A layout owns UI that should wrap a set of routes: navigation, an account shell, an admin sidebar, or shared providers.

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="dashboard-shell">
      <DashboardNavigation />
      <main>{children}</main>
    </div>
  );
}

Do not turn a layout into a global state container by default. A root layout is a poor place for page-specific fetching or client state because every child inherits that choice. Put providers as close as possible to the routes that require them, and keep server-rendered structure outside client providers when possible.

A Product Page Walkthrough

A practical route usually separates responsibilities like this:

  1. The page fetches the product and decides whether the route exists.
  2. Server-rendered components create the title, price, description, and SEO-relevant content.
  3. A client component owns quantity selection and the add-to-cart event.
  4. A route-level loading component gives the visitor feedback while the server work is pending.
  5. An error boundary offers a recovery path when the data operation fails.

This does not mean server and client code are enemies. The boundary is a contract. The server prepares trusted data and structure; the client receives the smallest serializable input necessary to make the experience responsive.

Common Boundary Mistakes

Fetching in a client effect by reflex. If data is required to render the route, start on the server. Client fetching is still useful for live updates, polling, and interaction-driven requests, but it should be a deliberate choice.

Passing server-only objects to a client component. Database connections, functions, class instances, and secrets cannot cross the boundary. Pass IDs, strings, numbers, plain objects, and arrays instead.

Treating use client as a per-function switch. It applies to the file and its client-side dependency graph. Put it in a focused leaf component rather than a broad route shell.

Using a layout when a page component is enough. A layout is durable by design. If a view should reset for every route, keep it in the page.

A Practical Decision Process

When you add a component, ask these questions in order:

  1. Does it need an event handler, local state, an effect, or a browser API? If not, keep it on the server.
  2. Does it need server-only data or credentials? If yes, it must remain on the server.
  3. Can a small client component receive serializable data from a server parent? If yes, isolate the interaction there.
  4. Should the UI persist across several routes? If yes, consider a layout.
  5. What should the visitor see while the segment loads or fails? Add route-level loading and error UI intentionally.

What Is Next

With the App Router mental model in place, the next post goes deeper into Server and Client Component boundaries: what crosses them, how bundle size changes, and how to design props that keep the relationship clean.

Key Takeaways

  • App Router routes are composed from nested route files and rendering boundaries.
  • Server Components are the default place for data access and non-interactive UI.
  • "use client" creates a client bundle boundary; keep it low in the component tree.
  • Layouts are durable shells, while pages own route-specific content.
  • Good App Router design is mostly about placing responsibilities on the right side of the boundary.