Last updated: 2026-10-08

Data Fetching

Every application that does something useful fetches data. User profiles, blog posts, product listings, dashboard metrics — they all come from somewhere, and how they are fetched determines the performance, reliability, and maintainability of the application that displays them.

Next.js does not prescribe a single data fetching method. It provides a model — server-first, close to the data, with caching built in — and a set of patterns that work within that model. Understanding those patterns, and knowing which one fits which situation, is what makes the difference between an application that feels fast and one that feels like it is constantly loading.


The Shift in How Data Fetching Works

In the Pages Router era, data fetching was structurally separate from rendering. A page component could not fetch its own data — data was fetched in specially exported functions (getServerSideProps, getStaticProps) that ran before rendering and passed the result into the page as props. The component and its data were deliberately decoupled.

The App Router eliminates this separation. Because page components are Server Components by default — running on the server, with access to server-side resources — they can fetch their own data directly, in the same place they use it, without the ceremony of a separate function and a props handoff.

// App Router — data fetching co-located with the component that uses it
export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());

  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  );
}

This is the natural shape of data fetching in the App Router. A component declares what data it needs, fetches it directly, and renders the result. The data never leaves the server. The client receives fully rendered HTML.


Fetching Data in Server Components

Server Components are where most data fetching in a Next.js application should happen. The reasons are practical: Server Components run on the server, close to the data source, without the latency of a client-to-server round trip, and with access to secrets and internal resources that should never be exposed to the browser.

Using fetch

The fetch API — the same one used in browsers — is available in Server Components and is the foundation of data fetching in Next.js. Next.js extends the native fetch with caching and revalidation capabilities, which are covered shortly.

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const product = await fetch(`https://api.example.com/products/${id}`).then(
    r => r.json()
  );

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>£{product.price}</p>
    </div>
  );
}

The fetch call happens on the server. The browser receives a fully rendered product page. No loading state, no skeleton screen, no second network request from the client. The data is there when the page arrives.

Using an ORM or Database Client Directly

Because Server Components run in a Node.js environment, they can import and use database clients, ORMs, and any other server-side library directly — without going through a public API endpoint.

import { db } from '@/lib/db';

export default async function UsersPage() {
  const users = await db.user.findMany({
    orderBy: { createdAt: 'desc' },
  });

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

This is direct database access from a React component — something that would have been unthinkable and unsafe in any previous React model. It is safe here because the component never runs in the browser. The database query happens on the server during rendering, and the result is what the client receives.


Parallel vs Sequential Data Fetching

When a component needs data from multiple sources, the order in which those fetches happen has a significant impact on performance.

Sequential Fetching — the Slow Path

Awaiting each fetch one after another creates a waterfall: the second fetch cannot begin until the first completes, the third cannot begin until the second completes, and so on. The total wait time is the sum of all individual fetches.

export default async function ProfilePage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  // Sequential — each waits for the previous to complete
  const user = await fetchUser(id);         // 100ms
  const posts = await fetchUserPosts(id);   // 100ms
  const followers = await fetchFollowers(id); // 100ms
  // Total: ~300ms
}

Sequential fetching is appropriate when the result of one fetch is required to make the next — when a user ID is needed to fetch that user's posts, for example. When there is no such dependency, it is unnecessary overhead.

Parallel Fetching — the Fast Path

Initiating all independent fetches simultaneously and awaiting them together with Promise.all() reduces the total wait time to the duration of the slowest individual fetch.

export default async function ProfilePage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  // Parallel — all three start at the same time
  const [user, posts, followers] = await Promise.all([
    fetchUser(id),
    fetchUserPosts(id),
    fetchFollowers(id),
  ]);
  // Total: ~100ms — the duration of the slowest request
}

The principle is identical to what was established in the async/await topic of the Node.js series: independent operations should run concurrently, not sequentially. The App Router brings that principle into component-level data fetching.


Caching and Revalidation

Raw data fetching without caching means every request to the server triggers every data fetch all over again — a database query, an external API call, a file read. For data that does not change between requests, this is pure waste. For data that changes occasionally, it means deciding how frequently to accept stale data in exchange for performance.

Next.js extends the fetch API with caching and revalidation options that address this directly.

Caching a Fetch Response

By default, Next.js caches fetch responses in the Data Cache — a server-side, persistent cache that survives across requests and deployments. A fetch that is cached returns the same response on every subsequent request, without hitting the data source again, until the cache is explicitly invalidated or the data is revalidated.

// Cached indefinitely — fetched once, reused on every request
const data = await fetch('https://api.example.com/config', {
  cache: 'force-cache',
});

This is appropriate for data that rarely or never changes — configuration values, reference data, static content.

Time-Based Revalidation

Data that changes occasionally — product listings, blog posts, dashboard aggregates — benefits from caching with a defined expiry: the cached value is used for a set period, after which the next request triggers a fresh fetch and updates the cache.

// Cached for one hour — fresh data fetched at most once per hour
const data = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 }, // seconds
});

After the revalidation window passes, the next request triggers a background refetch. The user who made that request still receives the cached data immediately — the refetch happens after the response is sent, so the user is never made to wait. Subsequent requests receive the newly cached data. This pattern is Incremental Static Regeneration at the data level: cached, fast, and kept fresh on a schedule.

Opting Out of Caching

Data that is specific to each request — user-specific data, real-time values, anything that must be fresh on every request — should not be cached.

// Not cached — fresh fetch on every request
const data = await fetch('https://api.example.com/user/me', {
  cache: 'no-store',
});

no-store ensures the data source is hit on every request. This is appropriate for authentication-sensitive data or anything where serving a stale response would be incorrect or misleading.


The Request Memoization

Next.js automatically memoizes fetch requests with the same URL and options within a single render pass. If two different components in the same component tree both fetch the same URL, the request is made only once — the result is shared between them.

// Both components fetch the same endpoint
async function Header() {
  const user = await fetch('/api/user/me').then(r => r.json());
  return <nav>{user.name}</nav>;
}

async function ProfileCard() {
  const user = await fetch('/api/user/me').then(r => r.json());
  return <aside>{user.avatar}</aside>;
}

Even though both components call fetch('/api/user/me'), the actual HTTP request is made once per render. The second call returns the memoized result from the first. This means data fetching can be written where it is logically needed — in the component that uses it — without worrying about redundant network calls caused by multiple components sharing the same data need.

This memoization is different from the Data Cache. The Data Cache persists between requests. Memoization exists only for the duration of a single render pass — it is reset for each new request.


Fetching Data in Client Components

Client Components fetch data differently, because they run in the browser without access to server-side resources. The options are:

Calling a Route Handler. The cleanest approach. A Client Component calls an endpoint defined in app/api/, which performs the server-side data fetching and returns the result as JSON.

'use client';

import { useState, useEffect } from 'react';

export default function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch('/api/user/me')
      .then(r => r.json())
      .then(setUser);
  }, []);

  if (!user) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}

Using a data fetching library. Libraries like SWR and TanStack Query manage client-side data fetching with built-in caching, revalidation, loading and error states, and optimistic updates. They are well-suited for data that needs to stay fresh as the user interacts with the page — dashboards, live feeds, search results.

'use client';

import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then(r => r.json());

export default function UserProfile() {
  const { data: user, error, isLoading } = useSWR('/api/user/me', fetcher);

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Failed to load.</p>;

  return <h1>{user.name}</h1>;
}

The general principle is that Client Components should fetch as little data as possible, and should do so through an API rather than directly touching a database or reading environment secrets.


Data Fetching Patterns at Scale

As an application grows, patterns for how data is fetched and shared across components become important.

Fetching at the Layout Level

Data needed by multiple pages within a route segment can be fetched in the layout, rather than duplicated in every page beneath it.

// app/dashboard/layout.tsx
export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const currentUser = await getCurrentUser();

  return (
    <div>
      <DashboardNav user={currentUser} />
      {children}
    </div>
  );
}

The user data is fetched once and used by the layout's navigation. Every page under /dashboard has access to the same user without each one needing to fetch it independently.

Streaming with Suspense

When a page has multiple sections that fetch data independently, streaming allows the parts that are ready to be sent to the browser first, while sections still awaiting data show loading placeholders. This is covered in the Loading and Error States topic, but it begins with data fetching: wrapping slow data fetches in separate components, each inside a <Suspense> boundary, allows the page to progressively fill in as data arrives.

import { Suspense } from 'react';
import { PostList } from '@/components/PostList';
import { RecentActivity } from '@/components/RecentActivity';

export default function DashboardPage() {
  return (
    <div>
      <Suspense fallback={<p>Loading posts...</p>}>
        <PostList />
      </Suspense>
      <Suspense fallback={<p>Loading activity...</p>}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

PostList and RecentActivity each fetch their own data. Whichever finishes first appears in the browser first. The user sees something immediately, rather than waiting for the slowest data source before anything renders.


Revalidating Cached Data on Demand

Time-based revalidation works for data that changes on a schedule. For data that changes in response to a specific event — a new post is published, a user updates their profile, an order is placed — on-demand revalidation is more precise.

Next.js provides two functions for this, called from Server Actions or Route Handlers:

import { revalidatePath, revalidateTag } from 'next/cache';

// Invalidate all cached data for a specific path
revalidatePath('/blog');

// Invalidate all cached fetch responses tagged with a specific key
revalidateTag('posts');

Tags are applied at the fetch level:

const posts = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});

When a new post is published through an admin action, calling revalidateTag('posts') purges the cached result for every fetch tagged 'posts' across the entire application. The next request to any page that uses that data triggers a fresh fetch and repopulates the cache. The content is updated without a redeploy.


Summary

PatternWhen to Use
fetch in a Server ComponentThe standard approach for external APIs — server-side, no client overhead
ORM/database client in a Server ComponentDirect database access without an intermediary API
Promise.all() for parallel fetchesMultiple independent data sources — avoid sequential waterfalls
cache: 'force-cache'Data that rarely changes — configuration, reference data
next: { revalidate: N }Data that changes on a schedule — product listings, blog posts
cache: 'no-store'Data that must be fresh on every request — user-specific, real-time
revalidatePath / revalidateTagOn-demand cache invalidation after a mutation
fetch in a Client Component via Route HandlerClient-side data that requires user interaction to trigger
SWR / TanStack QueryClient-side data with caching, live updates, and optimistic UI