Next.js Fetching Data
Fetching data means getting information from somewhere — a database, an external API, or a file — and displaying it on a page. Next.js gives you several ways to fetch data, and each method suits a different situation. Choosing the right one makes your app faster and more reliable.
Where You Fetch Data Matters
In Next.js, you can fetch data on the server or on the client. Server-side fetching happens before the page reaches the browser. Client-side fetching happens after the page loads in the browser. Server-side is faster and more secure for most cases.
SERVER-SIDE FETCH: Server fetches data → Builds HTML with data → Sends complete page to browser (User sees content immediately, no loading spinner) CLIENT-SIDE FETCH: Browser loads page → Page is empty → JS runs → Fetches data → Shows content (User sees a loading state first)
Fetching Data in a Server Component
Server components in Next.js can be async functions. You fetch data directly inside the component — no useEffect, no state management needed.
// app/blog/page.js (Server Component by default)
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<main>
<h1>Blog Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</main>
);
}
Next.js runs this code on the server. The data arrives before the HTML is sent to the browser. The user sees a complete page with no loading delay.
Diagram: Server Fetch vs Client Fetch
SERVER FETCH (async server component): Request arrives → Server runs fetch → Data ready → HTML built → Sent to browser Timeline: [===fetch===][==render==]→ Browser gets full page CLIENT FETCH (useEffect): Browser loads → Empty page shown → JS runs → Fetch starts → Data arrives → Page updates Timeline: [page loads] → [spinner] → [===fetch===] → [content appears]
Controlling Caching with fetch Options
Next.js extends the native fetch function with extra options that control how data is cached. You choose whether Next.js stores the fetched data for later or always fetches fresh data.
Cache Forever (Static)
const res = await fetch('https://api.example.com/posts', {
cache: 'force-cache' // Fetch once, reuse forever (default behavior)
});
Never Cache (Always Fresh)
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store' // Fetch fresh data on every request
});
Revalidate on a Schedule
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 } // Refetch data every 60 seconds
});
Diagram: Three Fetch Strategies
force-cache: Build time → Fetch once → Save → Serve same data to all users Good for: documentation, marketing pages no-store: Every request → Fresh fetch → Different data possible each time Good for: live feeds, dashboards, personalized pages revalidate: 60 First request → Fetch → Save for 60s → Serve cached After 60s → Next request triggers new fetch Good for: news sites, product listings
Fetching Data for Dynamic Pages
For dynamic routes, you fetch data using the URL parameter. The page receives params and uses it to request the right data.
// app/blog/[slug]/page.js
async function getPost(slug) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
cache: 'no-store'
});
if (!res.ok) throw new Error('Post not found');
return res.json();
}
export default async function PostPage({ params }) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
Fetching Data in Client Components
When you need data that changes based on user interaction — like search results that update as someone types — fetch data from a client component using useEffect and useState.
'use client';
import { useState, useEffect } from 'react';
export default function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) return;
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => setResults(data));
}, [query]);
return (
<ul>
{results.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
Parallel Data Fetching
When a page needs data from multiple sources, fetch them at the same time instead of one after another. Use Promise.all to run fetches in parallel.
// Sequential (slow): waits for each fetch to finish before starting the next const user = await fetchUser(id); const posts = await fetchPosts(id); // Total time = time for user + time for posts // Parallel (fast): both fetches start at the same time const [user, posts] = await Promise.all([ fetchUser(id), fetchPosts(id) ]); // Total time = whichever fetch takes longer
SEQUENTIAL: [===fetchUser===][===fetchPosts===] → 400ms total
PARALLEL: [===fetchUser===]
[===fetchPosts===] → 200ms total
Handling Fetch Errors
Always check if a fetch succeeded before using the data. A successful response returns a status code between 200 and 299.
async function getData(url) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to fetch: ${res.status}`);
}
return res.json();
}
Key Takeaway
Fetch data in server components for the fastest, most secure experience. Use the cache and next.revalidate options to control how fresh your data needs to be. Fetch in client components only when data depends on user interaction. Use Promise.all to fetch multiple sources at the same time and avoid unnecessary delays.
