Next.js
What is Next.js?
Next.js is React with everything you need already built in. React builds your UI. Next.js gives you the rest: pages, routing, data fetching and speed.
React alone: install a router, then wire up every page yourself.
<Route path="/about" element={<About />} />Next.js: make a folder, get a page.
app/about/page.tsx β /aboutWhy developers love it:
- Folders are pages: no router to set up
- Fetch data in your component: just await fetch(), no useEffect
- Fast by default: pages are built on the server, so they load quickly and show up on Google
- Batteries included: links, images, loading and 404 pages are built in
- Industry standard: React's own docs recommend it for new apps
How This Course Works
You learn the building blocks first, then we build a blog together, then you build your own app.
- Learn the blocks: each lesson solves one problem, and that solution shows you the next problem.
- Build with me: we put every block together into a real blog that loads posts from the internet.
- Build your own: you build a mini shop alone, with these notes as your map.
Before you start:
- Node.js 20.9 or newer
- VS Code
- Basic React: components, JSX, props
- π‘ Tailwind comes built in, so everything from your Tailwind notes works here
Module 1: The Setup
1. Your First Next.js App
One command gives you a running website.
npx create-next-app@latest my-first-appWhen it asks "Would you like to use the recommended Next.js defaults?", choose Yes. You get TypeScript, ESLint, Tailwind CSS, the App Router, and an AGENTS.md file.
cd my-first-appnpm run devOpen http://localhost:3000 in your browser.
- π‘ Leave npm run dev running. Every time you save a file, the browser updates by itself
2. The Tour
You only need these for now.
| File / Folder | What it does |
|---|---|
| package.json | The project's ID card: its name, scripts (dev, build, start), and installed packages |
| .gitignore | Files Git should never save, like node_modules and .next |
| public/ | Static files like images and icons. public/logo.png is served at /logo.png |
| app/ | Your website. Folders in here become URLs |
| app/globals.css | Global styles. Tailwind is imported here |
| app/page.tsx | The home page, shown at / |
| app/layout.tsx | The frame around every page. We come back to it in lesson 6 |
Everything else (next.config.ts, tsconfig.json, eslint.config.mjs, postcss.config.mjs, AGENTS.md, node_modules/) you can leave alone for now.
- π‘ Remember public/. We use it again in lesson 8
Module 2: The Building Blocks
3. Hello Next.js
Clear out the starter so the project is yours.
app/globals.css (delete everything else):
@import "tailwindcss";app/page.tsx:
export default function Home() { return ( <main className="p-8"> <h1 className="text-4xl font-bold">Hello Next.js</h1> </main> )}- π‘ A page is just a React component exported as default from page.tsx
- β οΈ In React it's className, not class
4. Routing
In Next.js, a folder is a URL. Put a page.tsx inside it, and the page exists.
app/βββ page.tsx β /βββ about/β βββ page.tsx β /aboutβββ contact/ βββ page.tsx β /contactapp/about/page.tsx:
export default function About() { return <h1 className="p-8 text-4xl font-bold">About</h1>}| Rule | Result |
|---|---|
| Folder name | Becomes the URL |
| page.tsx inside | Makes the page visible |
| No page.tsx | No page (404) |
| Folder in a folder | Nested URL: app/blog/news/page.tsx β /blog/news |
- π‘ These are static routes: the URL never changes. Lesson 10 covers URLs that change, like /posts/1
5. The Link Component
A plain <a> reloads the whole site on every click. <Link> only swaps the page content.
Why use it:
- No flash: the screen doesn't go white between pages
- Instant: links on screen are loaded before you click
- Shared parts stay: the navbar and layout never reload
import Link from "next/link"
<Link href="/about">About</Link>- π‘ Try both: click a plain <a> and watch the page flash, then switch to <Link>
- π‘ Use <Link> for pages inside your app
6. Layout
Write shared parts once. layout.tsx wraps every page, and children is the page being shown.
app/layout.tsx:
import Link from "next/link"import "./globals.css"
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <nav className="flex gap-4 p-4 border-b"> <Link href="/">Home</Link> <Link href="/about">About</Link> <Link href="/contact">Contact</Link> </nav> {children} </body> </html> )}- π‘ Change a link here once, and every page updates
- π‘ The starter layout also loads fonts. You can keep those lines
- β οΈ Only the root layout has <html> and <body>
7. Metadata
Give your site a title and description. They show in the browser tab and in Google results.
In app/layout.tsx (for every page):
import type { Metadata } from "next"
export const metadata: Metadata = { title: "My First App", description: "Learning Next.js step by step",}In app/about/page.tsx (for one page):
export const metadata = { title: "About" }| Where | Applies to |
|---|---|
| layout.tsx | Every page (the default) |
| page.tsx | That page only, overrides the layout |
8. The Image Component
<Image> is the Next.js version of <img>. It makes your images load faster, automatically.
Why use it:
- Auto resizing: each screen gets the right size, so a phone never downloads a desktop-sized photo
- Smaller files: a 5 MB photo is converted to WebP, a lighter format
- Faster pages: images load only when you scroll to them
- No jumping: space is reserved before the image arrives
- Put a photo in public/, for example public/hero.jpg
- Show it:
import Image from "next/image"
<Image src="/hero.jpg" alt="Team at work" width={800} height={400} />| Prop | Why |
|---|---|
| src | Path from public/, starts with / |
| alt | Describes the image for screen readers and Google |
| width / height | Reserves space so the page doesn't jump while loading |
Images from another website:
- Allow the website in next.config.ts:
const nextConfig = { images: { remotePatterns: [new URL("https://images.unsplash.com/**")], },}
export default nextConfig- Use the full URL as src:
<Image src="https://images.unsplash.com/your-photo" alt="Mountains" width={800} height={400} />- β οΈ Skip step 1 and Next.js shows an error: the website is not allowed
9. Data Fetching
Pages run on the server by default, so they can be async and fetch data directly.
app/posts/page.tsx:
type Post = { id: number; title: string; body: string }
export default async function PostsPage() { const res = await fetch("https://jsonplaceholder.typicode.com/posts") const posts: Post[] = await res.json()
return ( <main className="p-8"> <h1 className="text-3xl font-bold mb-4">Posts</h1> <ul className="space-y-2"> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> </main> )}- Make the component async
- await fetch(url)
- await res.json()
- map over the data
- π‘ No useEffect, no useState. The server fetches before the page reaches the browser
- β οΈ Every item in a list needs a unique key
10. Dynamic Routes
Square brackets make one page that works for every item.
app/posts/[id]/page.tsx β /posts/1, /posts/2, /posts/3 ...app/posts/[id]/page.tsx:
type Post = { id: number; title: string; body: string }
export default async function PostPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`) const post: Post = await res.json()
return ( <main className="p-8"> <h1 className="text-3xl font-bold">{post.title}</h1> <p>{post.body}</p> </main> )}Link to it from the list:
<Link href={`/posts/${post.id}`}>{post.title}</Link>- π‘ The name in the brackets is your choice: [id] gives you id, [slug] gives you slug
- β οΈ params is a Promise. Always await it. Older tutorials skip this and break
11. Not Found
Show a friendly 404 instead of a broken page.
In app/posts/[id]/page.tsx, right after the fetch:
import { notFound } from "next/navigation"
if (!res.ok) notFound()app/not-found.tsx:
import Link from "next/link"
export default function NotFound() { return ( <main className="p-8 text-center"> <h1 className="text-4xl font-bold">404</h1> <p className="mb-4">This page doesn't exist.</p> <Link href="/" className="underline">Go home</Link> </main> )}- π‘ not-found.tsx also catches any URL that has no page, like /banana
12. Loading
Show something while the data loads.
app/posts/loading.tsx:
export default function Loading() { return <p className="p-8">Loading...</p>}Next.js shows it automatically while the page in that folder is fetching.
- π‘ To see it, add a fake delay at the top of your page, then remove it after: await new Promise((r) => setTimeout(r, 2000))
- π‘ A loading.tsx covers its folder and every folder inside it
π Next.js 80/20 Cheatsheet
Everything from this course, in one place.
| Task | How |
|---|---|
| Create an app | npx create-next-app@latest my-app |
| Run it | npm run dev β localhost:3000 |
| Home page | app/page.tsx β / |
| New page | app/about/page.tsx β /about |
| Dynamic page | app/posts/[id]/page.tsx β /posts/1 |
| Read the URL | const { id } = await params |
| Navigate | <Link href="/about">About</Link> |
| Shared header/footer | app/layout.tsx with {children} |
| Title + description | export const metadata = { title, description } |
| Static files | public/logo.png β /logo.png |
| Image | <Image src="/logo.png" alt="..." width={200} height={100} /> |
| Fetch data | async page + await fetch(url) |
| 404 | notFound() + app/not-found.tsx |
| Loading | loading.tsx in the folder |
Special files:
| File | Job |
|---|---|
| page.tsx | Makes a URL visible |
| layout.tsx | Wraps pages with shared parts |
| loading.tsx | Shows while the page fetches |
| not-found.tsx | Shows when nothing is found |
Official docs: nextjs.org/docs
Challenge: Build a Mini Shop
Build Suuqify, a small online shop with real products. You've already learned every block you need.
- π Refer to the attached sketches for how the products page and the details page should look
The API:
| Endpoint | Gives you |
|---|---|
| https://fakestoreapi.com/products | All 20 products |
| https://fakestoreapi.com/products/1 | One product |
The fields you need from each product:
{ "id": 1, "title": "Fjallraven - Foldsack No. 1 Backpack", "price": 109.95, "description": "Your perfect pack for everyday use...", "image": "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"}The pages:
| Route | Shows |
|---|---|
| / | A welcome message and a link to the shop |
| /products | A grid of all products: image, title, price |
| /products/[id] | One product: image, title, price, description |
| /about | A few lines about your shop |
Your app must have:
- A shared layout with a navbar and footer
- Navigation with <Link>
- A products page that fetches all products
- Product cards that link to /products/[id]
- A details page that reads the id from params
- Product images with <Image>
- A title and description on every page
- A 404 for products that don't exist
- A loading state
Tips:
- π‘ Open the API in your browser first, so you see the data before you write any code
- π‘ Try /products/999 in your browser and see what the API sends back. That tells you when to call notFound()
- β οΈ The images come from fakestoreapi.com. Allow it in next.config.ts first:
images: { remotePatterns: [new URL("https://fakestoreapi.com/**")],},