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.

Leave a Comment