🟣 Data, Server & APIs · Lesson 19
Data Fetching & Caching
Fetching is just async/await
In the App Router you do not need useEffect or loading state for most data. Make the Server Component async and await your data — the HTML arrives already filled, which is perfect for SEO.
JSX – async Server Component
// app/page.jsx — a Server Component can be async
async function getStats() {
return { students: 1200, courses: 6, rating: 4.8 } // could be a DB call
}
export default async function Home() {
const stats = await getStats()
return <p>{stats.students}+ students · {stats.courses} courses · ⭐ {stats.rating}</p>
}Old React way (client)
- Empty HTML first
- useEffect → fetch in browser
- Loading spinner
- Google may see no content
Next.js Server Component
- Data fetched on server
- HTML arrives complete
- No spinner needed
- Perfect for SEO
Fetching from an API
JSX – app/jobs/page.jsx
// app/jobs/page.jsx
async function getJobs() {
const res = await fetch('https://api.example.com/jobs?city=delhi')
if (!res.ok) throw new Error('Failed to load jobs') // → error.jsx
return res.json()
}
export default async function Jobs() {
const jobs = await getJobs()
return (
<ul>
{jobs.map((j) => <li key={j.id}>{j.title} – {j.company}</li>)}
</ul>
)
}Reading local files (Markdown blog)
Server Components can use Node.js modules. EduSite's blog reads .md files from content/posts/:
Markdown – content/posts/how-to-start-coding.md
--- title: How to Start Coding in 2026 date: 2026-03-10 excerpt: A simple 30-day plan for complete beginners. --- ## Day 1–7: Pick one language Start with **Python** or **C** …
JS – lib/posts.js
// lib/posts.js (from the EduSite project)
import fs from 'node:fs'
import path from 'node:path'
import matter from 'gray-matter' // reads the --- front matter ---
import { marked } from 'marked' // converts Markdown → HTML
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts')
export function getAllPosts() {
return fs.readdirSync(POSTS_DIR)
.filter((f) => f.endsWith('.md'))
.map((file) => {
const slug = file.replace(/\.md$/, '')
const { data } = matter(fs.readFileSync(path.join(POSTS_DIR, file), 'utf8'))
return { slug, title: data.title, date: data.date, excerpt: data.excerpt }
})
.sort((a, b) => (a.date < b.date ? 1 : -1)) // newest first
}
export function getPost(slug) {
const file = path.join(POSTS_DIR, slug + '.md')
if (!/^[a-z0-9-]+$/.test(slug) || !fs.existsSync(file)) return null
const { data, content } = matter(fs.readFileSync(file, 'utf8'))
return { slug, ...data, html: marked.parse(content) }
}
Caching fetch results
JS – four caching options
// 1) No caching (default) – fresh on every request
await fetch('https://api.example.com/price')
// 2) Cache forever (until next deploy or revalidate)
await fetch('https://api.example.com/states', { cache: 'force-cache' })
// 3) Cache, but refresh at most every 1 hour (ISR-style)
await fetch('https://api.example.com/courses', { next: { revalidate: 3600 } })
// 4) Cache with a tag so you can refresh it on demand
await fetch('https://api.example.com/courses', { next: { tags: ['courses'] } })| Data type | Best option |
|---|---|
| Live price, user dashboard | no cache (default) |
| List of states, rarely changes | force-cache |
| Blog posts, course list | revalidate: 3600 |
| Admin edits courses | tags + revalidateTag |
Revalidate: fresh data on a schedule
JS – route segment config
// app/blog/page.jsx — whole page regenerates at most every 10 minutes export const revalidate = 600
On-demand revalidation with tags
JS – app/admin/actions.js
// app/admin/actions.js
'use server'
import { revalidateTag, revalidatePath } from 'next/cache'
export async function addCourse(formData) {
await saveCourseToDatabase(formData)
revalidateTag('courses', 'max') // every fetch tagged 'courses' is stale now
revalidatePath('/courses') // or: refresh one path
}Parallel vs sequential requests
JS – Promise.all
// ❌ Sequential: 1s + 1s + 1s = 3 seconds const course = await getCourse(slug) const reviews = await getReviews(slug) const teacher = await getTeacher(slug) // ✅ Parallel: all start together ≈ 1 second const [course, reviews, teacher] = await Promise.all([ getCourse(slug), getReviews(slug), getTeacher(slug), ])
Sequential
- getCourse ▓▓▓
- getReviews ▓▓▓
- getTeacher ▓▓▓
- ≈ 3 s
Parallel (Promise.all)
- getCourse ▓▓▓
- getReviews ▓▓▓
- getTeacher ▓▓▓
- ≈ 1 s
Fetching in Client Components
Use browser fetching only for data that changes while the user watches (live seats, chat) or depends on user actions.
JSX – components/LiveSeats.jsx
// components/LiveSeats.jsx — refresh seat count every 10s in the browser
'use client'
import { useEffect, useState } from 'react'
export default function LiveSeats({ slug }) {
const [seats, setSeats] = useState(null)
useEffect(() => {
const load = () => fetch(`/api/seats?course=${slug}`).then((r) => r.json()).then((d) => setSeats(d.left))
load()
const id = setInterval(load, 10000)
return () => clearInterval(id)
}, [slug])
return <p>{seats === null ? 'Checking seats…' : `${seats} seats left`}</p>
}Common mistakes
- Calling your own
/api/...route from a Server Component → unnecessary HTTP hop; call the function (e.g.getCourses()) directly. - Expecting fetch to be cached automatically (old Next.js 13/14 behaviour) → add
force-cacheorrevalidate. - Awaiting independent requests one by one → use
Promise.all. - Not checking
res.ok→ a 500 error page is parsed as JSON and crashes.
Summary
- Server Components:
async+awaitdata directly (API, files, database). - Caching is opt-in:
force-cache,revalidate,tags. revalidateTag/revalidatePathafter updates;Promise.allfor speed.
✅ Practice
Create content/posts with 3 Markdown files and show them on /blog sorted by date. Then fetch a public API with next: { revalidate: 60 } and show the time it was fetched.
Frequently Asked Questions
Is fetch cached by default in Next.js 16?
No. Since Next.js 15, fetch requests are not cached by default. Add cache: 'force-cache' or next: { revalidate: seconds } to cache a response.
Do I need useEffect to load data in Next.js?
Not for most pages. Make the Server Component async and await the data. useEffect is only for data that must load in the browser after user interaction.
What is revalidateTag?
A function you call after data changes (for example in a Server Action). It marks all fetches with that tag as stale, so the next visitor gets fresh data. In Next.js 16 pass a profile: revalidateTag('courses', 'max').
