Menu

8. Data Fetching

Next.js Master Roadmap - IT Technology

This chapter explores how to fetch data in Next.js using the extended Fetch API, covering server‑side and client‑side patterns, caching, revalidation, and integration with REST, GraphQL, and third‑party services. You’ll learn concrete techniques for ISR, tag‑based invalidation, parallel fetching, and protecting secrets while building performant applications.

No MCQ questions available for this chapter.

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 = 0 for SSR or const revalidate = 3600 for ISR at the top of a page/layout file.

Revalidation Flow:

  1. A mutation occurs (e.g., creating a post).
  2. You call revalidateTag('posts') (or path‑based).
  3. Next.js purges the cache entries associated with that tag/path.
  4. The next request to a cached route triggers a fresh fetch.
  5. 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

  1. 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}

    ); }
  2. 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');
    }
    
  3. 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) return 

    Loading posts…

    ; if (error) return

    Failed to load posts.

    ; return (
      {data.map(post => (
    • {post.title}

      {post.excerpt}

    • ))}
    ); }
  4. 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.all to 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 revalidateOnFocus and revalidateIfStale to match your caching strategy.