Skip to main content
Ganesh Joshi
Back to Blogs

Getting started with the Next.js App Router

February 9, 20266 min read
Tutorials
Code editor or development workspace

The Next.js App Router is the modern way to build React applications with Next.js. It uses React Server Components by default, supports streaming, and provides a more intuitive file-based routing system. This guide covers everything you need to start building with the App Router.

File-based routing

In the App Router, the app directory defines your routes. Each folder represents a URL segment, and special files determine what renders at that route.

File Purpose
page.tsx The UI for a route (makes the route publicly accessible)
layout.tsx Shared UI that wraps pages and nested layouts
loading.tsx Loading UI shown while page content loads
error.tsx Error UI shown when something fails
not-found.tsx UI for 404 errors
route.ts API endpoint (replaces pages/api)

Basic routes

app/
├── page.tsx           # /
├── about/
│   └── page.tsx       # /about
├── blog/
│   ├── page.tsx       # /blog
│   └── [slug]/
│       └── page.tsx   # /blog/my-post
└── contact/
    └── page.tsx       # /contact

A folder without page.tsx doesn't create a route. You can use folders purely for organization.

Dynamic routes

Use square brackets for dynamic segments:

app/
├── blog/
│   └── [slug]/
│       └── page.tsx   # /blog/anything
├── users/
│   └── [id]/
│       └── page.tsx   # /users/123

Access the parameter in the page:

// app/blog/[slug]/page.tsx
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  // Fetch post by slug
  return <article>Post: {slug}</article>;
}

Catch-all routes

For routes with multiple segments:

app/
└── docs/
    └── [...slug]/
        └── page.tsx   # /docs/a, /docs/a/b, /docs/a/b/c

The slug parameter is an array: ['a', 'b', 'c'].

Optional catch-all with double brackets matches the parent too:

app/
└── docs/
    └── [[...slug]]/
        └── page.tsx   # /docs, /docs/a, /docs/a/b

Layouts

Layouts wrap pages and persist across navigations. They don't re-render when navigating between pages they contain.

Root layout

Every app needs a root layout with <html> and <body>:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <nav>Site Navigation</nav>
        <main>{children}</main>
        <footer>Site Footer</footer>
      </body>
    </html>
  );
}

Nested layouts

Add layout files in route segments for nested layouts:

app/
├── layout.tsx         # Root layout (all pages)
├── (marketing)/
│   ├── layout.tsx     # Marketing layout
│   ├── page.tsx       # /
│   └── about/
│       └── page.tsx   # /about
└── dashboard/
    ├── layout.tsx     # Dashboard layout
    ├── page.tsx       # /dashboard
    └── settings/
        └── page.tsx   # /dashboard/settings

Dashboard pages get both the root layout and dashboard layout.

Route groups

Parentheses create route groups that don't affect the URL:

app/
├── (marketing)/
│   ├── layout.tsx     # Shared marketing layout
│   ├── page.tsx       # / (not /marketing)
│   └── pricing/
│       └── page.tsx   # /pricing
└── (app)/
    ├── layout.tsx     # Shared app layout
    └── dashboard/
        └── page.tsx   # /dashboard

Route groups let you organize code and share layouts without adding URL segments.

Server and client components

By default, all components in the App Router are React Server Components. They run only on the server.

Server component benefits

  • Direct database access
  • Secure environment variables
  • No JavaScript sent to browser
  • Async/await in component body
// This is a server component (default)
export default async function ProductList() {
  const products = await db.query('SELECT * FROM products');
  
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

When to use client components

Add "use client" at the top of a file to make it a client component:

'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

Use client components when you need:

Feature Example
React state useState, useReducer
Effects useEffect, useLayoutEffect
Browser APIs window, localStorage, navigator
Event handlers onClick, onChange, onSubmit
Custom hooks with state useForm, useQuery

Composition pattern

Keep server components as parents, pass data to client components as props:

// app/products/page.tsx (server component)
import { ProductFilter } from './ProductFilter';

export default async function ProductsPage() {
  const categories = await db.query('SELECT * FROM categories');
  
  return (
    <div>
      <ProductFilter categories={categories} />
      {/* Server-rendered product list */}
    </div>
  );
}

// app/products/ProductFilter.tsx (client component)
'use client';

export function ProductFilter({ categories }) {
  const [selected, setSelected] = useState(null);
  // Interactive filter UI
}

Data fetching

Server components can fetch data directly with async/await:

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 }, // Revalidate every hour
  }).then((res) => res.json());
  
  return <PostList posts={posts} />;
}

Fetch options

Next.js extends fetch with caching options:

Option Behavior
{ cache: 'force-cache' } Cache indefinitely (default)
{ cache: 'no-store' } Don't cache, always fetch fresh
{ next: { revalidate: 60 } } Cache for 60 seconds, then revalidate
{ next: { tags: ['posts'] } } Tag for on-demand revalidation

Database queries

Query databases directly in server components:

import { db } from '@/lib/db';

export default async function UsersPage() {
  const users = await db.select().from(usersTable);
  return <UserList users={users} />;
}

No API route needed. The query runs on the server during render.

Loading and error states

Loading UI

Add loading.tsx for automatic loading states:

// app/dashboard/loading.tsx
export default function Loading() {
  return <div className="animate-pulse">Loading dashboard...</div>;
}

This shows while page.tsx loads, using React Suspense under the hood.

Error handling

Add error.tsx for error boundaries:

'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Error boundaries must be client components to use the reset function.

Not found

Add not-found.tsx for 404 states:

// app/not-found.tsx
export default function NotFound() {
  return (
    <div>
      <h1>404</h1>
      <p>Page not found</p>
    </div>
  );
}

Trigger it programmatically with notFound() from next/navigation.

Metadata

Export a metadata object or generateMetadata function:

// Static metadata
export const metadata = {
  title: 'My Page',
  description: 'Page description',
};

// Dynamic metadata
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);
  
  return {
    title: post.title,
    description: post.excerpt,
  };
}

Metadata merges with parent layouts. Child metadata overrides parent fields.

Navigation

Use the Link component for client-side navigation:

import Link from 'next/link';

export default function Nav() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
      <Link href="/blog">Blog</Link>
    </nav>
  );
}

For programmatic navigation, use the useRouter hook in client components:

'use client';

import { useRouter } from 'next/navigation';

export function LoginButton() {
  const router = useRouter();
  
  async function handleLogin() {
    await login();
    router.push('/dashboard');
  }
  
  return <button onClick={handleLogin}>Login</button>;
}

Summary

The App Router uses file-based routing with the app directory. Components are server components by default—add "use client" only when you need interactivity. Use layouts for shared UI, loading.tsx for loading states, and error.tsx for error boundaries. Fetch data directly in server components with async/await. The official Next.js documentation has complete API references and examples.

Frequently Asked Questions

The App Router is Next.js's modern routing system introduced in Next.js 13. It uses the app directory with file-based routing, supports React Server Components by default, and provides features like layouts, loading states, and streaming.

The App Router uses the app directory with server components by default, nested layouts, and streaming. The Pages Router uses the pages directory with client components by default and getServerSideProps/getStaticProps for data fetching.

A layout.tsx file wraps pages and persists across navigations. It's useful for shared UI like headers and sidebars. Layouts can nest, with each route segment having its own layout that wraps child segments.

Add 'use client' when you need browser APIs (window, localStorage), React hooks (useState, useEffect), event handlers (onClick, onChange), or third-party libraries that use browser features.

In server components, use async/await directly with fetch or database queries. Next.js extends fetch with caching options. For client components, use React Query, SWR, or useEffect with fetch.

Related Posts