HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
5 Topics
Chapter•115 Minutes

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.

Fetch Data on the Server
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} />;
}
💡 The server retrieves data before rendering the component.
Key Takeaways
  • 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.

Parallel Requests
const [users, revenue, notifications] = await Promise.all([
  getUsers(),
  getRevenue(),
  getNotifications()
]);
💡 All independent operations begin without waiting for the previous operation to finish.
Key Takeaways
  • 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.

Typical Caching Layers
Key Takeaways
  • 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?

Invalidating a Path
'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');
}
💡 After a mutation, related paths can be marked for fresh rendering.
Key Takeaways
  • 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.

Data Access Layer
import 'server-only';

export async function getOrders(userId: string) {
  return db.order.findMany({
    where: { userId },
    orderBy: { createdAt: 'desc' }
  });
}
💡 A dedicated server-only data-access function keeps database details away from UI components.
Key Takeaways
  • Server Components can access databases directly.
  • Keep database logic in a dedicated service or repository layer.
  • Never expose database credentials to the browser.