Content
npm create-next-app@latest product-catalogue --yes
cd product-catalogue
npm run dev
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 file src/app/products/page.tsx
Add a type for the product data at the top of the file:
type Product = {
id: number
title: string
price: number
}
Export an async default function called ProductsPage
Inside it, fetch from the API and parse the JSON:
const products: Product[] = await fetch('<https://fakestoreapi.com/products>')
.then(r => r.json())
Return a <ul> that maps over products and renders each title and price in a <li>
Navigate to http://localhost:3000/products and confirm the list appears
src/app/products/[id]/page.tsxtype Product = {
id: number
title: string
price: number
description: string
image: string
}
Export an async default function called ProductPage that receives params as a prop:
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
}
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())
Return JSX that displays the title, price, description, and image
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} />
Open next.config.ts and add fakestoreapi.com to remotePatterns:
const nextConfig = {
images: {
remotePatterns: [
{ hostname: 'fakestoreapi.com' },
],
},
}
Visit http://localhost:3000/products/1 and http://localhost:3000/products/7 — each should show that product's details
src/app/products/page.tsx, add import Link from 'next/link' at the top<Link href={/products/${productId}} so clicking it goes to the detail pagesrc/app/products/[id]/page.tsx, add a <Link href="/products">← Back to products</Link> at the top of the returned JSXsrc/app/products/loading.tsxexport default function Loading() {
return <p>Loading products...</p>
}
To see it in action, add an artificial delay at the top of ProductsPage in page.tsx:
await new Promise(resolve => setTimeout(resolve, 1500))
Navigate to /products — you should see "Loading products..." for 1.5 seconds before the list appears
Remove the delay once you've confirmed it works
In src/app/products/[id]/page.tsx, add this import at the top:
import { notFound } from 'next/navigation'
After fetching the product, check if it's missing and call notFound():
if (!product || !product.id) notFound()
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>
)
}
Visit http://localhost:3000/products/9999 — you should see the not-found page instead of a broken render
src/app/users/[id]/page.tsxtype User = {
id: number
name: string
email: string
company: { name: string }
}
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())
Render the user's name, email, and company name
Visit http://localhost:3000/users/1 and http://localhost:3000/users/3 — each should show a different user
Create src/components/FollowButton.tsx with "use client" at the top
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>
)
}
Import and render <FollowButton name={user.name} /> in your UserPage Server Component
Confirm the button toggles state on click, while the user data (name, email) came from the server
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}}>
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>
)
}
Navigate between the list and detail pages. The "Users" heading should stay in place without re-rendering