The App Router is a fundamentally different mental model from the Pages Router. Once the server-vs-client component distinction clicks, everything else follows. Here's the complete picture.

File System Routing

The app/ directory defines routes through folder structure:

app/
├── layout.tsx          → / (root layout, wraps everything)
├── page.tsx            → / (home page)
├── globals.css
├── templates/
│   ├── layout.tsx      → /templates (templates layout)
│   ├── page.tsx        → /templates
│   └── [id]/
│       └── page.tsx    → /templates/123
├── blog/
│   ├── page.tsx        → /blog
│   └── [slug]/
│       └── page.tsx    → /blog/my-post
└── api/
    └── templates/
        └── route.ts    → /api/templates (API route)

Special files:

  • page.tsx — the visible UI for a route
  • layout.tsx — shared UI that wraps child pages
  • loading.tsx — shown while the page is loading (Suspense boundary)
  • error.tsx — error boundary for the route
  • not-found.tsx — 404 UI

Server Components — The Default

Every component in app/ is a Server Component by default. They can be async, fetch data directly, and never ship JavaScript to the browser:

// app/templates/page.tsx — Server Component
interface Template {
  id: number
  name: string
  framework: string
  price: number
}

// No 'use client' — this runs on the server at build/request time
export default async function TemplatesPage() {
  // fetch runs server-side — no CORS, no loading state in browser
  const templates = await fetch('https://api.lettstartdesign.com/templates', {
    next: { revalidate: 3600 } // cache for 1 hour
  }).then(r => r.json()) as Template[]

  return (
    <main className="container py-4">
      <h1 className="fw-bold mb-4">Templates</h1>
      <div className="row g-4">
        {templates.map(t => (
          <div key={t.id} className="col-md-4">
            <div className="card border-0 shadow-sm h-100">
              <div className="card-body p-4">
                <span className="badge mb-2" style={{ background: '#fd4766' }}>{t.framework}</span>
                <h5 className="fw-bold">{t.name}</h5>
                <p className="fw-bold mt-auto" style={{ color: '#fd4766' }}>${t.price}</p>
              </div>
            </div>
          </div>
        ))}
      </div>
    </main>
  )
}

Client Components — When You Need Interactivity

Add 'use client' at the top of any component that uses state, effects, or event listeners:

// app/templates/TemplateFilter.tsx — Client Component
'use client'

import { useState } from 'react'

export default function TemplateFilter({
  frameworks,
  onFilter
}: {
  frameworks: string[]
  onFilter: (fw: string) => void
}) {
  const [active, setActive] = useState('All')

  function select(fw: string) {
    setActive(fw)
    onFilter(fw)
  }

  return (
    <div className="d-flex gap-2 flex-wrap mb-4">
      {['All', ...frameworks].map(fw => (
        <button
          key={fw}
          onClick={() => select(fw)}
          className={`btn btn-sm ${active === fw ? 'text-white' : 'btn-outline-secondary'}`}
          style={active === fw ? { background: '#fd4766', border: 'none' } : {}}
        >
          {fw}
        </button>
      ))}
    </div>
  )
}

Layouts — Shared UI Across Routes

// app/layout.tsx — Root layout (required)
import type { Metadata } from 'next'
import './globals.css'
import 'bootstrap/dist/css/bootstrap.min.css'

export const metadata: Metadata = {
  metadataBase: new URL('https://bootstrapplanet.com'),
  title: { default: 'BootstrapPlanet', template: '%s | BootstrapPlanet' },
  description: 'Free Bootstrap 5 components, Angular and React examples.',
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
// app/templates/layout.tsx — Templates section layout
export default function TemplatesLayout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <nav className="navbar navbar-expand-lg bg-dark navbar-dark px-4">
        <a className="navbar-brand fw-bold" href="/">LettStart</a>
        <div className="navbar-nav ms-auto">
          <a className="nav-link" href="/templates">Templates</a>
          <a className="nav-link" href="/angular">Angular</a>
        </div>
      </nav>
      <main>{children}</main>
    </div>
  )
}

Layouts persist between navigations — the navbar above doesn't re-mount when navigating from /templates to /templates/123.

Dynamic Routes

// app/templates/[id]/page.tsx
interface Template { id: number; name: string; framework: string; description: string }

// generateStaticParams — pre-generates all template pages at build time
export async function generateStaticParams() {
  const templates = await fetch('/api/templates').then(r => r.json())
  return templates.map((t: Template) => ({ id: String(t.id) }))
}

// generateMetadata — dynamic SEO metadata per template
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const template = await fetch(`/api/templates/${id}`).then(r => r.json())
  return {
    title: template.name,
    description: template.description,
    alternates: { canonical: `https://bootstrapplanet.com/templates/${id}` }
  }
}

export default async function TemplatePage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const template: Template = await fetch(`/api/templates/${id}`).then(r => r.json())

  return (
    <main className="container py-4">
      <h1 className="fw-bold">{template.name}</h1>
      <span className="badge" style={{ background: '#fd4766' }}>{template.framework}</span>
      <p className="mt-3">{template.description}</p>
    </main>
  )
}

Loading and Error States

// app/templates/loading.tsx — shown during page load (automatic Suspense)
export default function Loading() {
  return (
    <div className="container py-4">
      <div className="row g-4">
        {[1, 2, 3, 4, 5, 6].map(i => (
          <div key={i} className="col-md-4">
            <div className="card border-0 shadow-sm" style={{ height: '180px' }}>
              <div className="card-body p-4">
                <div className="placeholder-glow">
                  <span className="placeholder col-4 mb-3" style={{ background: '#eee', height: '20px', display: 'block', borderRadius: '4px' }}></span>
                  <span className="placeholder col-8" style={{ background: '#eee', height: '16px', display: 'block', borderRadius: '4px' }}></span>
                </div>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}
// app/templates/error.tsx — must be a Client Component
'use client'

export default function Error({
  error, reset
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div className="container py-4">
      <div className="alert alert-danger d-flex align-items-center gap-3">
        <span>⚠️</span>
        <div>
          <strong>Something went wrong</strong>
          <p className="mb-0 small">{error.message}</p>
        </div>
        <button className="btn btn-sm btn-danger ms-auto" onClick={reset}>Retry</button>
      </div>
    </div>
  )
}

API Routes

// app/api/templates/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const framework = searchParams.get('framework')

  const templates = [
    { id: 1, name: 'Marvel Dashboard', framework: 'Angular', price: 49 },
    { id: 2, name: 'PORTO', framework: 'Bootstrap', price: 29 },
  ].filter(t => !framework || t.framework === framework)

  return NextResponse.json(templates)
}

export async function POST(request: NextRequest) {
  const body = await request.json()
  // create template...
  return NextResponse.json({ id: 3, ...body }, { status: 201 })
}

Key Concepts Summary

ConceptRule
Server ComponentDefault — async, no browser APIs, no useState/useEffect
Client ComponentAdd 'use client' — has state, events, browser APIs
LayoutWraps child pages, persists between navigations
loading.tsxAuto Suspense fallback while page loads
error.tsxError boundary — must be 'use client'
generateStaticParamsPre-renders all dynamic routes at build time
generateMetadataPer-page SEO metadata — title, description, canonical
next: { revalidate }ISR — rebuild cache after N seconds

Frequently Asked Questions

The App Router (app/ directory, Next.js 13+) uses React Server Components by default, supports nested layouts, and colocates data fetching with components using async/await. The Pages Router (pages/ directory) uses getServerSideProps/getStaticProps for data fetching and has a flat layout system. The App Router is the current recommended approach for new projects.
Add 'use client' only when a component needs browser APIs, React state (useState/useReducer), effects (useEffect), or event listeners. Everything else should stay as a Server Component. A good rule: start server, add 'use client' only when Next.js or TypeScript complains about using client-only APIs.
In Server Components, just use async/await directly in the component: const data = await fetch('...').then(r => r.json()). No useEffect, no loading state needed. For client-side fetching after mount, use SWR, TanStack Query, or useEffect as usual.
Routes are statically rendered at build time by default (like getStaticProps). They become dynamic if you use cookies(), headers(), or searchParams in a Server Component. You can force static with export const dynamic = 'force-static' or force dynamic with 'force-dynamic'.

Need a Full Bootstrap 5 Admin Dashboard?

Get a complete Angular 21 + Bootstrap 5 dashboard with 50+ components — built by the same team behind BootstrapPlanet.

Browse Templates →

Use code FIRST30 for 30% off your first purchase.

Related Components