8. Data Fetching
8.1 Fetch API Fundamentals
Next.js builds on the native fetch function, adding next options for caching and revalidation. Understanding the core verbs, response handling, and error patterns is essential before diving into framework‑specific features.
8.1.1 GET Requests
A GET request retrieves data. By default, Next.js treats fetch calls in Server Components as cache: 'force-cache', enabling static generation unless overridden.
const res = await fetch(url, {
next: { revalidate: 60, tags: ['posts'] }
});
// Returns a Response object; use .json() or .text()
const data = await res.json();
Example without explicit options (defaults to SSG):
const data = await fetch('https://api.example.com/posts')
.then(r => r.json());
8.1.2 POST Requests
POST requests send data to the server. They are commonly used in Server Actions and Route Handlers.
await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Hello' })
});
8.1.3 Error Handling
Always check res.ok (status 200‑299). Network errors reject the promise; HTTP errors do not.
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (e) {
// fallbackData could be a default object or null
return fallbackData;
}
8.2 Next.js Data Fetching Patterns
Next.js distinguishes between Server Components (default) and Client Components ('use client'). Server Components run on the server, have zero client bundle, and can directly access databases, secrets, and the filesystem. Client Components rely on browser‑side fetching or libraries like SWR/React Query.
8.2.1 Server-side Fetching
Data fetching in Server Components occurs at render time, enabling SEO‑friendly HTML and reducing client‑side waterfall.
async function Page() {
const posts = await db.post.findMany();
return ;
}
Because the function runs on the server, you can safely use environment variables and perform expensive operations without exposing them to the browser.
8.2.2 Client-side Fetching
In Client Components, data fetching must happen after mounting, typically via useEffect or a data‑fetching library.
'use client';
import useSWR from 'swr';
function fetcher(url) { return fetch(url).then(r => r.json()); }
export function ClientPosts() {
const { data, error, isLoading } = useSWR('/api/posts', fetcher);
if (isLoading) return Loading…
;
if (error) return Failed to load.
;
return (
{data.map(p => - {p.title}
)}
);
}
8.2.3 Caching
Next.js extends fetch with a next object that controls caching behavior. The cache key is derived from URL, method, headers, and next options.
- cache: 'force-cache' (default) → Static Site Generation (SSG); cached until revalidated.
- cache: 'no-store' → Server‑Side Rendering (SSR); fresh data on every request.
- next: { revalidate: N } → Incremental Static Regeneration (ISR); cache persists for N seconds then revalidates in background.
- next: { tags: ['tag'] } → Enables tag‑based invalidation via
revalidateTag('tag'). - next: { revalidate: false } → Never revalidate; static forever.
fetch(url, {
next: { tags: ['products'], revalidate: 3600 }
});
8.2.4 Revalidation
Revalidation purges cached data so the next request triggers a fresh fetch. Next.js supports several strategies.
- Time‑based (ISR):
next: { revalidate: 60 }revalidates after 60 seconds. - On‑demand – Tag‑based: Call
revalidateTag('posts')in a Server Action or Route Handler after a mutation. - On‑demand – Path‑based:
revalidatePath('/blog')revalidates all segments matching the path. - Manual segment config: Export
const revalidate = 0for SSR orconst revalidate = 3600for ISR at the top of a page/layout file.
Revalidation Flow:
- A mutation occurs (e.g., creating a post).
- You call
revalidateTag('posts')(or path‑based). - Next.js purges the cache entries associated with that tag/path.
- The next request to a cached route triggers a fresh fetch.
- The new data is fetched, HTML is regenerated, and the updated cache is stored.
8.3 API Integration Patterns
Regardless of the API style, you can leverage fetch with next options to control caching and revalidation.
8.3.1 REST APIs
Standard HTTP verbs map naturally to fetch. Centralizing calls in a lib/api.ts file promotes reuse and consistent caching.
export async function getProducts() {
return fetch(`${process.env.API_URL}/products`, {
next: { tags: ['products'], revalidate: 3600 }
}).then(res => res.json());
}
8.3.2 GraphQL
GraphQL uses a single endpoint with POST bodies containing queries or mutations. You can still attach tags for cache control.
fetch(GRAPHQL_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query Products {
products { id name price }
}
`
}),
next: { tags: ['products'] }
}).then(r => r.json());
8.3.3 Third‑party APIs
When calling external services, keep secrets server‑side, handle rate limits, and implement proper error handling.
const weather = await fetch(
`https://api.weather.com/v1?key=${process.env.WEATHER_KEY}&city=NYC`,
{ next: { revalidate: 1800 } }
).then(r => r.json());
Key Formulas & Patterns
- Cache Key:
url + method + headers + JSON.stringify(next)→ deterministic key used by Next.js’s internal cache. - Revalidation Flow: Mutation →
revalidateTag('tag')→ cache purge → next request triggers fresh fetch → new HTML cached. - Parallel Fetching:
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()])— reduces waterfall latency. - Sequential Fetching:
const user = await fetchUser(id); const posts = await fetchPosts(user.id)— when later requests depend on earlier data.
Concrete Examples
- ISR Product Page
export async function generateStaticParams() { const products = await fetch(`${API}/products`, { next: { tags: ['products'] } }) .then(r => r.json()); return products.map(p => ({ slug: p.slug }; } export default async function Page({ params }) { const product = await fetch( `${API}/products/${params.slug}`, { next: { revalidate: 3600, tags: ['products'] } } ).then(r => r.json()); return ( ); }{product.title}
{product.description}
- Server Action Mutation + Revalidation
'use server'; export async function createPost(formData) { await db.post.create({ data: { title: formData.get('title'), content: formData.get('content') } }); // Invalidate all cached posts revalidateTag('posts'); // Optionally revalidate a specific path revalidatePath('/posts'); redirect('/posts'); } - Client‑side SWR Hook
'use client'; import useSWR from 'swr'; function fetcher(url) { return fetch(url).then(r => r.json()); } export function PostList() { const { data, error, isLoading } = useSWR('/api/posts', fetcher, { // Optional: enable revalidation on focus revalidateOnFocus: true, }); if (isLoading) returnLoading posts…
; if (error) returnFailed to load posts.
; return (-
{data.map(post => (
-
{post.title}
{post.excerpt}
))}
-
- GraphQL Query with Tags
fetch(GRAPHQL_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: ` query GetProducts { products(limit: 10) { id name price stock } } ` }), next: { tags: ['products'], revalidate: 0 } // SSR for fresh data }).then(r => r.json());
Best Practices Checklist
- Prefer Server Components for data that does not require interactivity.
- Use
next: { revalidate: N }for ISR when data changes infrequently. - Leverage tag‑based revalidation to purge only affected caches after mutations.
- Keep API keys and secrets in environment variables accessed only in Server Components or Route Handlers.
- Handle network and HTTP errors gracefully with try/catch and fallback UI.
- Parallelize independent requests with
Promise.allto reduce latency. - For dependent data, use sequential fetching or consider GraphQL to fetch related data in a single round‑trip.
- When using SWR/React Query, configure
revalidateOnFocusandrevalidateIfStaleto match your caching strategy.