Skip to main content
Next.js Tutorial

Next.js App Router Complete Guide: Step-by-Step Tutorial

Learn how modern Next.js routing works by building a small product catalog, from the first layout to dynamic product pages and production safeguards.

Next.js App Router guide represented by a modern application dashboard

The Next.js App Router turns folders into URLs and gives every part of an application a clear place for its page, shared layout, loading screen, error recovery, and metadata. It can feel unusual when you arrive from a client-only React app because rendering is server-first. Once the mental model clicks, however, many decisions that used to require extra libraries become ordinary project structure.

This tutorial builds a fictional shop named Trail Supply. Its catalog illustrates static routes, dynamic product URLs, server data, a client-side cart button, and route-level user experience. The goal is not merely to make links work. We will create routes that remain understandable, fast, accessible, secure, and discoverable as the product grows.

SEO profile: Meta title: Next.js App Router Guide: Step-by-Step Tutorial (2026). Meta description: Learn the Next.js App Router step by step with layouts, dynamic routes, navigation, SEO, security, performance tips, errors, and a real project. URL slug: nextjs-app-router-complete-guide. Focus keyword: Next.js App Router guide. Related keywords: Next.js App Router tutorial, Next.js 16 routing, nested layouts, dynamic routes, Server Components.

1. Why the App Router exists

A growing React application needs more than a map from a path to a component. It needs shared shells that do not reset unnecessarily, server access to data and secrets, predictable loading feedback, isolated error handling, document metadata, and sensible code splitting. Building each concern independently creates repeated configuration and makes teams invent conventions that new developers must learn.

The App Router makes the URL hierarchy the backbone of those concerns. A dashboard folder can own its navigation layout and error boundary. A product folder can own its metadata and missing-product behavior. React Server Components keep database work on the server, while small Client Components add interaction where the browser is genuinely needed.

This design suits content sites, stores, SaaS dashboards, documentation, marketplaces, and authenticated portals. A tiny one-page widget may not need it. For a multi-page product, the consistent conventions usually pay back the initial learning cost.

2. How App Router routing works

Next.js reads the app directory as a tree of route segments. Ordinary folders contribute URL segments. A page.tsx makes the current segment visitable, while layout.tsx wraps every descendant. The root layout is required and supplies the document's html and body elements.

Route tree
app/
|-- layout.tsx                 shared document shell
|-- page.tsx                   /
|-- about/
|   `-- page.tsx               /about
|-- products/
|   |-- layout.tsx             wraps catalog routes
|   |-- loading.tsx            catalog fallback UI
|   |-- page.tsx               /products
|   `-- [slug]/
|       |-- page.tsx           /products/trail-pack
|       `-- not-found.tsx      missing product UI
`-- api/
    `-- availability/
        `-- route.ts           /api/availability

Notice that file names carry meaning. You do not import page.tsx into a router configuration. Next.js assembles the component tree from the folders. Other files can live beside a page without becoming routes, which makes feature-based colocation practical.

3. Create the project step by step

Install Node.js 20.9 or newer, then use the project generator. The recommended defaults currently include TypeScript, ESLint, Tailwind CSS, the App Router, Turbopack, and an @/* import alias.

Terminal
npx create-next-app@latest trail-supply --yes
cd trail-supply
npm run dev

Open http://localhost:3000. Keep the generated lockfile, run the linter separately, and test a production build early:

Quality checks
npm run lint
npm run build
npm start

Next.js 16 no longer runs a linter automatically as part of next build, so both commands belong in continuous integration. Turbopack is the default for development and builds; old --turbo flags are unnecessary.

4. Build pages and nested layouts

Start with the root layout. Static metadata works well when every route shares a brand template.

app/layout.tsx
import type { Metadata } from 'next'
import Link from 'next/link'
import './globals.css'

export const metadata: Metadata = {
  metadataBase: new URL('https://trailsupply.example'),
  title: { default: 'Trail Supply', template: '%s | Trail Supply' },
  description: 'Reliable outdoor equipment for weekend adventures.',
}

export default function RootLayout({ children }: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en">
      <body>
        <a href="#content" className="skip-link">Skip to content</a>
        <header><Link href="/">Trail Supply</Link></header>
        <main id="content">{children}</main>
      </body>
    </html>
  )
}

Now add app/products/layout.tsx with a category navigation and a children slot. It wraps both the product list and individual product pages. Because layouts persist during client navigation, a filter panel or navigation selection can remain stable while the page below it changes.

Use template.tsx only when you need the opposite behavior. A template gets a fresh component instance during navigation, so local state resets and mount effects run again. Replacing every layout with a template creates needless work and can make navigation feel jumpy.

5. Create dynamic product routes

A bracketed folder represents data rather than a literal path. [slug] matches one segment, [...slug] matches one or more segments, and [[...slug]] also matches the parent path with no value. Product detail pages need the single-segment form.

app/products/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { getProduct } from '@/lib/products'

type ProductPageProps = {
  params: Promise<{ slug: string }>
}

export default async function ProductPage({ params }: ProductPageProps) {
  const { slug } = await params
  const product = await getProduct(slug)

  if (!product) notFound()

  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>${product.price.toFixed(2)}</p>
    </article>
  )
}

In current Next.js, params is a promise. Code copied from older tutorials may access params.slug synchronously and fail type checks or trigger migration warnings. Await it before use. Validate the returned value as well; a URL is untrusted input, even when TypeScript says it is a string.

For a finite catalog, generateStaticParams can provide known slugs for prerendering. Do not generate millions of rarely visited pages during every deployment. Prerender popular or stable content, then choose an appropriate request-time or caching strategy for the long tail.

6. Combine Server and Client Components

Pages and layouts are Server Components unless you declare a client boundary. That allows getProduct to query a database directly without shipping credentials or data-fetching code to the browser. It also reduces JavaScript because plain rendered content does not need a client bundle.

The Add to cart control does need state and an event handler. Isolate that behavior in a small file:

app/products/_components/AddToCart.tsx
'use client'

import { useState } from 'react'

export function AddToCart({ productId }: { productId: string }) {
  const [added, setAdded] = useState(false)

  return (
    <button type="button" onClick={() => setAdded(true)} disabled={added}>
      {added ? 'Added to cart' : 'Add to cart'}
    </button>
  )
}

Import this button into the server-rendered product page and pass only serializable props. The underscore folder communicates that _components is private and never routable. A common mistake is adding 'use client' to the whole page because of one button. That pulls its imported module graph toward the browser and gives up server-only advantages.

Use Link from next/link for internal destinations. Next.js can prefetch likely destinations, reuse shared layouts, and update the route without a full document reload. In production, static routes are normally prefetched as links enter the viewport. Dynamic routes can stream behind a loading boundary.

Product card link
import Link from 'next/link'

export function ProductLink({ slug, name }: { slug: string; name: string }) {
  return <Link href={`/products/${encodeURIComponent(slug)}`}>{name}</Link>
}

Disable prefetching only for a measured reason, such as a huge set of low-probability links. Use useRouter from next/navigation for navigation caused by application logic, not as a replacement for accessible links. Never send an untrusted string to router.push; a malicious URL scheme could become a client-side injection path.

Route groups such as (marketing) organize files or apply different layouts without appearing in the URL. Private folders such as _components exclude an entire branch from routing. Avoid defining two grouped pages that resolve to the same public URL.

Advanced routing: parallel and intercepted routes

Parallel slots, named with folders such as @modal, render multiple route branches in one layout. Intercepting patterns can show a destination inside the current context. Together they support a product quick-view modal that opens over the catalog during client navigation while the same product URL renders a full page after refresh or direct sharing. This is excellent for galleries and login dialogs, but it adds route-state complexity. Build it only after the ordinary detail page works.

8. Loading, errors, and missing pages

A resilient route designs more than its success state. loading.tsx provides instant fallback UI and creates a Suspense boundary. Match the skeleton dimensions to the final interface to reduce layout shift, and include text such as “Loading products” so the state is understandable without animation.

error.tsx catches failures within its segment. It must be a Client Component because it receives an error and a reset callback. Show a calm message and a retry button, log a safe error identifier on the server, and never print stack traces, SQL details, tokens, or personal data.

Call notFound() when a requested product does not exist and create not-found.tsx with navigation back to the catalog. This produces a meaningful 404 experience rather than a misleading empty product page. Reserve redirects for content that genuinely moved.

9. SEO considerations for App Router pages

Routing affects search visibility because every useful piece of content needs a stable, crawlable URL. Use descriptive lowercase paths, avoid putting essential navigation behind click handlers, and ensure category pages link to product pages with meaningful anchor text.

Export metadata for fixed routes and generateMetadata for data-driven routes. Reuse the product lookup so the title and page do not perform unrelated duplicate work.

Dynamic product metadata
import type { Metadata } from 'next'

export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
  const { slug } = await params
  const product = await getProduct(slug)

  if (!product) return { title: 'Product not found' }

  return {
    title: product.name,
    description: product.summary,
    alternates: { canonical: `/products/${slug}` },
    openGraph: { images: [product.socialImage] },
  }
}

Add app/sitemap.ts, app/robots.ts, icons, and route-specific Open Graph images where useful. Give each page one clear H1, a unique title and description, semantic content, and correct status codes. Structured data must describe visible content; adding unsupported review ratings is spam, not optimization. For broader strategy, see our SEO guide and services.

10. Accessibility tips

  • Include a skip link and landmarks so keyboard and screen-reader users can bypass repeated navigation.
  • Use anchors for destinations and buttons for actions. Styling does not change an element's meaning.
  • Give every route a logical heading outline and keep focus indicators visible.
  • When a modal route opens, move focus into it, trap focus appropriately, label the dialog, and return focus when it closes.
  • Announce important cart or loading updates with a restrained live region; do not make every update interruptive.
  • Test soft navigation with a keyboard and screen reader. The absence of a full page reload can change expected focus announcements.

Accessibility is easiest when included in component contracts. A reusable product card should require an image description, a descriptive link label, and a valid heading level rather than relying on every caller to remember them.

11. Performance and security

Performance tips

Keep interactive islands small, fetch independent server data in parallel, and stream slow sections behind intentional Suspense boundaries. Use next/image with accurate dimensions and responsive sizes. Avoid marking every image as priority. Test npm run build followed by npm start, because development behavior is not a performance benchmark.

Prefetching is already intelligent; excessive manual prefetch calls can waste bandwidth. Measure Core Web Vitals and inspect route bundles before adding memoization or caching. In Next.js 16, caching is a deliberate architectural choice. Do not assume an old tutorial's caching defaults still apply. Our performance optimization overview explains the measurement-first approach.

Security tips

  • Treat route parameters, search parameters, cookies, headers, and form data as untrusted.
  • Authorize every server mutation against the current user; hiding a button is not authorization.
  • Keep secrets in server-only environment variables. Anything prefixed NEXT_PUBLIC_ is public.
  • Encode URL segments and validate redirect destinations against an allowlist.
  • Sanitize trusted-rich-text workflows before using dangerouslySetInnerHTML.
  • Patch Next.js, React, Node.js, and dependencies promptly and rate-limit sensitive endpoints.

12. Common errors and solutions

ProblemLikely causeSolution
Route returns 404The folder has no page.tsx, or the path casing differsAdd a default-exported page and use consistent lowercase URLs.
Hook or event-handler errorA Server Component uses client-only behaviorMove the interactive portion into a small file beginning with 'use client'.
Async params warningCode uses an older synchronous route patternType params as a promise and await it.
Conflicting routeTwo route groups resolve to the same URL, or page.tsx and route.ts claim one segmentGive each public path one owner.
Hydration mismatchInitial client output depends on time, randomness, or browser stateRender a stable initial value and read browser state after mount when necessary.
Secret appears in browserIt was imported through a client boundary or named NEXT_PUBLIC_*Rotate the secret and move access into server-only code.

13. Beginner and advanced best practices

Beginner tips

  1. Start with pages, layouts, Link, and one dynamic route.
  2. Keep data fetching in Server Components and add client boundaries only for interaction.
  3. Design loading, empty, error, and not-found states while building the happy path.
  4. Run type checks, linting, and a production build before every release.
  5. Name routes after user concepts, not internal database tables.

Advanced tips

  1. Split large applications by feature or route group, while keeping public URLs stable.
  2. Place Suspense boundaries around meaningful visual regions instead of every component.
  3. Use parallel and intercepted routes only when URL-addressable UI truly improves the experience.
  4. Centralize typed data access and validation so metadata and pages share trustworthy records.
  5. Monitor real navigation timings, server latency, client bundle size, errors, and cache behavior after deployment.

Choosing patterns for real-world applications

A marketing site usually benefits from a simple root layout, mostly static pages, route-specific metadata, and a dynamic blog segment. Keep its navigation crawlable and resist adding client state to content that can be plain HTML. An online store needs nested catalog layouts, product parameters, purposeful loading states, and server-side authorization for checkout. A SaaS dashboard often needs route groups for public and authenticated areas, plus narrow client boundaries for charts, filters, and drag-and-drop tools.

Documentation sites are a natural fit for an optional catch-all route such as docs/[[...slug]], because the same page component can handle both the documentation home and deeply nested topics. A photo feed may justify intercepted and parallel routes: clicking a photo opens a URL-backed modal, while visiting that URL directly renders the complete photo page. The correct pattern follows the user journey; advanced syntax is not a goal by itself.

Migrating an existing Pages Router project

You do not need a risky all-at-once rewrite. The pages and app directories can coexist, provided they do not claim the same URL. Begin with a low-risk route, create its App Router layout and page, then move its data fetching from Pages Router functions into Server Components. Replace next/router imports with the relevant APIs from next/navigation, and move head metadata into the Metadata API.

Test direct visits, refreshes, browser history, authenticated states, analytics events, canonical URLs, and error responses before moving the next route. Shared components may continue to work, but a component that uses hooks or browser APIs needs a client boundary. Do not place 'use client' on an entire migrated tree merely to silence one error; locate the actual interactive dependency.

Preserve public URLs whenever possible. If a URL must change, add a permanent redirect and update internal links, canonical metadata, and the sitemap together. Monitor 404s and search traffic after release. A technically correct migration can still harm users when old bookmarks, campaign links, or search results lead nowhere.

14. AI workflow with ChatGPT, Claude, and Codex

AI tools are useful collaborators when you give them boundaries and verify their output. ChatGPT can explain a route tree or turn acceptance criteria into test cases. Claude is helpful for reviewing a long architecture proposal and spotting inconsistent assumptions. Codex can inspect an actual repository, implement scoped route changes, and run the project's checks.

A practical workflow is: describe the URL and user journey, ask for the smallest route tree, request one component at a time, then verify every API against the installed Next.js version. Give the tool your package.json, existing folder structure, TypeScript rules, and accessibility requirements. Ask it to identify Server and Client boundaries explicitly.

Prompt example: “Using Next.js 16 App Router and TypeScript, propose a minimal route tree for a catalog with categories and product slugs. Keep pages server-rendered, isolate cart interaction in a Client Component, include loading/not-found states, and explain security assumptions. Do not invent packages.”

Never paste production secrets, customer records, private source code without permission, or unreviewed database exports into an AI chat. Treat generated code as an untrusted draft: inspect imports, confirm version-specific APIs, test error paths, run security checks, and keep a human responsible for the final design.

15. Real project blueprint and summary

For Trail Supply, the home page introduces the brand, /products lists inventory, and /products/[slug] owns every product. The products layout supplies category navigation. A loading file streams a stable catalog shell, a not-found file handles retired slugs, and a small Add to cart Client Component provides interaction. Dynamic metadata gives each product its own search snippet and share image.

This blueprint scales cleanly. Add (account) for authenticated pages without leaking organizational names into URLs. Add Route Handlers for webhooks or public HTTP APIs, but never place a route handler beside a page at the same segment. Consider a parallel modal only when product quick views become a demonstrated user need.

Summary: folders define segments; pages expose URLs; layouts persist around descendants; Server Components are the default; Client Components provide browser behavior; special files handle waiting, failure, and missing content; metadata describes each route; and optimized navigation reuses the route tree. Those are the App Router fundamentals worth mastering first.

Frequently asked questions

Is the App Router recommended for a new Next.js project?

Yes. For a new application, the App Router is the normal starting point. It supports layouts, Server Components, streaming, modern metadata, and route-level loading and error states.

Can the App Router and Pages Router exist in one project?

Yes. The app and pages directories can coexist, which makes gradual migration possible. They must not both define the same URL.

Does every folder inside app become a public route?

No. A segment becomes publicly reachable only when it contains a page or route file. You can safely colocate components and utilities, and private folders beginning with an underscore are explicitly excluded from routing.

Are App Router pages Server Components?

Pages and layouts are Server Components by default. Add use client only at a boundary that needs state, effects, event handlers, custom client hooks, or browser APIs.

Why must params be awaited in Next.js 16?

Current route props and request APIs are asynchronous. Awaiting params aligns your code with streaming and request-time rendering and avoids errors caused by older synchronous examples.

What is the difference between layout.tsx and template.tsx?

A layout persists as users move between its child routes, while a template creates a new instance on navigation. Use a template when child state or effects should reset.

When should I add loading.tsx?

Add it around a route segment that may wait for data. It supplies immediate fallback UI and creates a Suspense boundary that lets the ready parts of the route appear first.

Should I use Link or a regular anchor?

Use Link for internal application routes so Next.js can perform optimized client transitions and prefetching. Use an anchor for external sites, downloads, and non-HTTP protocols such as mailto.

How do dynamic routes affect SEO?

Dynamic routes can rank like static routes when each URL returns useful indexable content, a unique title and description, a canonical URL, correct status codes, and crawlable internal links.

Do I need parallel and intercepted routes as a beginner?

Usually not. Learn pages, layouts, dynamic segments, loading states, and navigation first. Parallel and intercepted routes are valuable for advanced dashboards and shareable modal experiences.

External references

Conclusion

The App Router is easiest to learn as a hierarchy, not a bag of APIs. Build the URL tree first, place persistent interface in layouts, keep data and secrets on the server, and add browser JavaScript at narrow interactive boundaries. Then make every route honest about waiting, failure, missing content, metadata, and authorization.

Your next exercise should be small: build the Trail Supply structure with three products, inspect the generated HTML, navigate with JavaScript disabled, test by keyboard, and run a production build. Once that works, you are ready for the next topic in the series: Server Components and Client Components in depth.

Need help with a Next.js project?

NavTech Solution can help plan a fast, accessible, and search-friendly application.

Discuss your project