Data Fetching, Caching and Revalidation
Learn server-side data fetching, fetch caching, request memoization, cache invalidation, parallel requests, waterfalls, and data-access architecture.
Fetching Data in Server Components
Server Components are a natural place to retrieve data because they can execute close to backend resources and do not require exposing API credentials to the browser.
A page can directly call a backend service, database abstraction, or external API. This can eliminate unnecessary browser-to-Next.js-to-backend request chains.
For application architecture, distinguish between fetching data for rendering and creating an API for other clients. You do not need to create an API route merely to fetch data inside your own Server Component.
export default async function ProductsPage() {
const response = await fetch('[https://example.com/api/products');
](https://example.com/api/products'%29;
) if (!response.ok) {
throw new Error('Failed to load products');
}
const products = await response.json();
return <ProductList products={products} />;
}- Fetch render data on the server when possible.
- Avoid creating unnecessary internal API hops.
- Handle failed responses explicitly.
Avoiding Request Waterfalls
A request waterfall occurs when independent requests are unnecessarily executed one after another.
Suppose a dashboard needs users, revenue, and notifications. If those requests do not depend on one another, executing them sequentially increases total latency.
Use Promise.all or equivalent parallelization when independent data sources can be requested simultaneously.
const [users, revenue, notifications] = await Promise.all([
getUsers(),
getRevenue(),
getNotifications()
]);- Identify independent requests and execute them concurrently.
- Waterfalls often become visible as latency grows.
- Do not parallelize operations that genuinely depend on one another.
Understanding the Caching Model
Caching in Next.js is not a single global cache. Different layers can exist, including browser caching, CDN caching, server-side data caching, request memoization, and application-level caches.
When debugging stale data, ask which layer contains the stale value. A page can appear stale because the data request is cached, because generated output is reused, or because an external CDN retains a response.
Caching should be intentional. Sensitive user-specific responses should not accidentally be shared across users.
- Caching has multiple layers.
- Always identify which layer is producing stale content.
- User-specific data requires careful cache boundaries.
Cache Invalidation
A cache is useful only if you can define when cached information should become invalid. Next.js provides mechanisms for invalidating cached data and routes after mutations.
A product editing workflow is a good example. After changing a product in the database, related product pages, category listings, and search results may need invalidation.
The important design question is dependency mapping: which cached resources depend on this mutation?
'use server';
import { revalidatePath } from 'next/cache';
export async function updateProduct(id: string, name: string) {
await database.products.update({ id, name });
revalidatePath(`/products/${id}`);
revalidatePath('/products');
}- Every cached resource should have a freshness strategy.
- Mutations should identify affected cached views.
- Overly broad invalidation can waste server resources.
Direct Database Access from Server Components
Because Server Components execute on the server, they can call database libraries directly. This can be more efficient than calling your own HTTP API from the server.
For example, a dashboard page can call getOrders() which uses Prisma, Mongoose, Drizzle, or another database abstraction. The browser receives rendered results rather than database credentials or internal query logic.
However, database access should be placed behind a clean data-access layer so components do not become collections of raw queries.
import 'server-only';
export async function getOrders(userId: string) {
return db.order.findMany({
where: { userId },
orderBy: { createdAt: 'desc' }
});
}- Server Components can access databases directly.
- Keep database logic in a dedicated service or repository layer.
- Never expose database credentials to the browser.