Getting Started with Next.js App Router

2024-02-01
Admin
Tutorial
#nextjs#react#typescript

Learn how to build modern web applications with Next.js App Router. This tutorial covers project setup, routing, layouts, data fetching, and server components.

Getting Started with Next.js App Router

Next.js has become one of the most popular React frameworks, and with the introduction of the App Router in Next.js 13, it has taken a massive leap forward. In this tutorial, we will walk through everything you need to know to get started with the App Router and build modern, performant web applications.

Prerequisites

Before we dive in, make sure you have the following:

  • Node.js 18.17 or later installed
  • Basic knowledge of React and TypeScript
  • A code editor (we recommend VS Code)
  • Familiarity with the terminal/command line

Creating a New Project

Let us start by scaffolding a new Next.js project with TypeScript support:

npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir
cd my-app
npm run dev

This command sets up a project with TypeScript, Tailwind CSS, ESLint, the App Router, and a src directory structure. Your development server will start at http://localhost:3000.

Understanding the App Router Directory Structure

The App Router uses a file-system based routing approach within the app directory. Here is what a typical project structure looks like:

src/
  app/
    layout.tsx        # Root layout (wraps all pages)
    page.tsx          # Home page (/)
    globals.css       # Global styles
    about/
      page.tsx        # About page (/about)
    blog/
      page.tsx        # Blog listing (/blog)
      [slug]/
        page.tsx      # Individual blog post (/blog/my-post)
    api/
      hello/
        route.ts      # API route (/api/hello)

The key conventions are:

  • page.tsx -- Defines the UI for a route and makes it publicly accessible.
  • layout.tsx -- Shared UI that wraps child pages and preserves state across navigations.
  • loading.tsx -- Loading UI shown while a page or layout is being rendered.
  • error.tsx -- Error boundary that catches errors in child components.
  • not-found.tsx -- UI shown when a route is not matched.

Server Components vs Client Components

One of the most important concepts in the App Router is the distinction between Server Components and Client Components.

Server Components (Default)

By default, all components in the app directory are React Server Components. They run on the server and have several advantages:

// app/blog/page.tsx
// This is a Server Component by default -- no directive needed

interface Post {
  id: number;
  title: string;
  excerpt: string;
}

async function getPosts(): Promise<Post[]> {
  const res = await fetch("https://api.example.com/posts", {
    next: { revalidate: 3600 }, // Revalidate every hour
  });
  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <main>
      <h1>Blog</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
          </li>
        ))}
      </ul>
    </main>
  );
}

Benefits of Server Components include:

  • Direct data fetching -- You can use async/await directly in the component.
  • Reduced bundle size -- Server-only code never gets sent to the client.
  • Security -- Sensitive data like API keys stays on the server.
  • SEO -- Content is rendered on the server, making it indexable by search engines.

Client Components

When you need interactivity (event handlers, state, browser APIs), you use Client Components by adding the "use client" directive:

"use client";

import { useState } from "react";

interface LikeButtonProps {
  initialCount: number;
}

export default function LikeButton({ initialCount }: LikeButtonProps) {
  const [likes, setLikes] = useState(initialCount);

  return (
    <button
      onClick={() => setLikes((prev) => prev + 1)}
      className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
    >
      Likes: {likes}
    </button>
  );
}

Layouts and Nesting

Layouts are a powerful feature that allow you to share UI across multiple pages. The root layout is required and wraps your entire application:

// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "My Next.js App",
  description: "Built with the App Router",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <header className="p-4 bg-gray-900 text-white">
          <nav>My App</nav>
        </header>
        <main className="container mx-auto p-4">{children}</main>
        <footer className="p-4 bg-gray-100 text-center">
          &copy; 2024 My App
        </footer>
      </body>
    </html>
  );
}

You can also create nested layouts for specific sections of your app:

// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-4 gap-8">
      <aside className="col-span-1">
        <h3>Categories</h3>
        <ul>
          <li>React</li>
          <li>TypeScript</li>
          <li>Next.js</li>
        </ul>
      </aside>
      <div className="col-span-3">{children}</div>
    </div>
  );
}

Dynamic Routes

Dynamic routes are created using square bracket notation in folder names:

// app/blog/[slug]/page.tsx
interface BlogPostProps {
  params: {
    slug: string;
  };
}

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  if (!res.ok) {
    throw new Error("Post not found");
  }
  return res.json();
}

export async function generateStaticParams() {
  const posts = await fetch("https://api.example.com/posts").then((res) =>
    res.json()
  );

  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({ params }: BlogPostProps) {
  const post = await getPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.date}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

The generateStaticParams function enables Static Site Generation (SSG) by pre-rendering pages at build time.

Loading and Error States

The App Router makes it simple to handle loading and error states with special files:

// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse space-y-4">
      <div className="h-8 bg-gray-200 rounded w-3/4"></div>
      <div className="h-4 bg-gray-200 rounded w-full"></div>
      <div className="h-4 bg-gray-200 rounded w-5/6"></div>
    </div>
  );
}
// app/blog/error.tsx
"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="p-4 bg-red-50 border border-red-200 rounded">
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button
        onClick={() => reset()}
        className="mt-2 px-4 py-2 bg-red-500 text-white rounded"
      >
        Try again
      </button>
    </div>
  );
}

Summary

Here is a quick recap of what we covered:

| Concept | Description | |---------|-------------| | App Router | File-system based routing in the app directory | | Server Components | Default components that run on the server | | Client Components | Interactive components using "use client" directive | | Layouts | Shared UI that wraps child pages | | Dynamic Routes | Routes with parameters using [param] folders | | Loading/Error States | Built-in UI for async operations and error handling |

The App Router represents a significant evolution in how we build React applications. It embraces server-first rendering while still providing the interactivity users expect. Start building with it today and experience the difference!


In the next post, we will take a deep dive into TypeScript generics -- a powerful feature that will level up your type safety game.