Week 11

App Router

Data Fetching and Mutations

Migrating from React Router

Going to production

Rendering Strategies

Practice

Assignment

Front end Track

Content

Let’s get practical

Exercise 1: Build a Product Catalogue Route

Setup

  1. Create a new Next.js project:
npm create-next-app@latest product-catalogue --yes
cd product-catalogue
npm run dev
  1. Open http://localhost:3000 and confirm the default Next.js page loads.

You'll use this public API for fake product data: https://fakestoreapi.com/products

Create the products list route

  1. Create the file src/app/products/page.tsx

  2. Add a type for the product data at the top of the file:

    type Product = {
      id: number
      title: string
      price: number
    }
    
  3. Export an async default function called ProductsPage

  4. Inside it, fetch from the API and parse the JSON:

    const products: Product[] = await fetch('<https://fakestoreapi.com/products>')
    																		.then(r => r.json())
    
  5. Return a <ul> that maps over products and renders each title and price in a <li>

  6. Navigate to http://localhost:3000/products and confirm the list appears

Add a product detail route

  1. Create the file src/app/products/[id]/page.tsx
  2. Add a type for a single product with more fields:
type Product = {
  id: number
  title: string
  price: number
  description: string
  image: string
}
  1. Export an async default function called ProductPage that receives params as a prop:

    export default async function ProductPage({
      params,
    }: {
      params: Promise<{ id: string }>
    }) {
    }
    
  2. Await the params to get the id, then fetch that product:

    const { id } = await params
    const product: Product = await fetch(`https://fakestoreapi.com/products/${id}`).then(r => r.json())
    
  3. Return JSX that displays the title, price, description, and image

  4. For the image, use next/image instead of <img>:

    import Image from 'next/image'
    
    <Image src={product.image} alt={product.title} width={200} height={200} />
    
  5. Open next.config.ts and add fakestoreapi.com to remotePatterns:

    const nextConfig = {
      images: {
        remotePatterns: [
          { hostname: 'fakestoreapi.com' },
        ],
      },
    }
    
  6. Visit http://localhost:3000/products/1 and http://localhost:3000/products/7 — each should show that product's details


Add navigation

  1. In src/app/products/page.tsx, add import Link from 'next/link' at the top
  2. Wrap each product title in a <Link href={/products/${productId}} so clicking it goes to the detail page
  3. In src/app/products/[id]/page.tsx, add a <Link href="/products">← Back to products</Link> at the top of the returned JSX
  4. Click between pages and confirm navigation works without a full page reload (watch the browser address bar - it should update without the tab flickering)

Add a loading state

  1. Create the file src/app/products/loading.tsx
  2. Export a default function that returns a simple loading message:
export default function Loading() {
  return <p>Loading products...</p>
}
  1. To see it in action, add an artificial delay at the top of ProductsPage in page.tsx:

    await new Promise(resolve => setTimeout(resolve, 1500))
    
  2. Navigate to /products — you should see "Loading products..." for 1.5 seconds before the list appears

  3. Remove the delay once you've confirmed it works


Handle missing products

  1. In src/app/products/[id]/page.tsx, add this import at the top:

    import { notFound } from 'next/navigation'
    
  2. After fetching the product, check if it's missing and call notFound():

    if (!product || !product.id) notFound()
    
  3. Create the file src/app/products/[id]/not-found.tsx:

    export default function NotFound() {
      return (
        <div>
          <h2>Product not found</h2>
          <p>This product does not exist.</p>
        </div>
      )
    }
    
  4. Visit http://localhost:3000/products/9999 — you should see the not-found page instead of a broken render


Exercise 2: Mix Server and Client Components

Fetch the user on the server

  1. Create src/app/users/[id]/page.tsx
  2. Add a type for the user:
type User = {
  id: number
  name: string
  email: string
  company: { name: string }
}
  1. Export an async UserPage component that reads the id param and fetches from https://jsonplaceholder.typicode.com/users/${id}:

    const user: User = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    													.then(r => r.json())
    
  2. Render the user's name, email, and company name

  3. Visit http://localhost:3000/users/1 and http://localhost:3000/users/3 — each should show a different user


Add a Client Component for the follow button

  1. Create src/components/FollowButton.tsx with "use client" at the top

  2. Use useState to toggle between followed and not followed:

    "use client"
    
    import { useState } from 'react'
    
    export function FollowButton({ name }: { name: string }) {
      const [following, setFollowing] = useState(false)
    
      return (
        <button onClick={() => setFollowing(f => !f)}>
          {following ? `Following ${name}` : `Follow ${name}`}
        </button>
      )
    }
    
  3. Import and render <FollowButton name={user.name} /> in your UserPage Server Component

  4. Confirm the button toggles state on click, while the user data (name, email) came from the server


Add a users list with a shared layout

  1. Create src/app/users/page.tsx that fetches all users from https://jsonplaceholder.typicode.com/users and renders each as a <Link href={/users/${user.id}}>

  2. Create src/app/users/layout.tsx with a heading that appears on both the list and detail pages:

    export default function UsersLayout({ children }: { children: React.ReactNode }) {
      return (
        <div>
          <h1>Users</h1>
          {children}
        </div>
      )
    }
    
  3. Navigate between the list and detail pages. The "Users" heading should stay in place without re-rendering