Data Fetching

Learn server-side data fetching, caching, and revalidation strategies

Data Fetching in Server Components

Next.js extends the native fetch API with caching and revalidation options. In Server Components, you can fetch data directly using async/await without useEffect or external libraries.

🗄️ Caching Options

force-cache - Cache indefinitely (default for static)
no-store - Never cache, always fresh
next: { revalidate: N } - Revalidate every N seconds
next: { tags: [...] } - Tag-based revalidation

Basic Data Fetching

// app/posts/page.tsx - Server Component
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    // Caching options
    cache: 'force-cache', // Default - cache forever
    // OR
    cache: 'no-store', // Always fetch fresh data
  });
  
  if (!res.ok) throw new Error('Failed to fetch posts');
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();
  
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Time-Based Revalidation (ISR)

// Revalidate data every 60 seconds
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 }, // Seconds
  });
  return res.json();
}

// Page-level revalidation - affects all fetches
export const revalidate = 60; // Revalidate page every 60 seconds

// Dynamic behavior
export const dynamic = 'force-dynamic'; // Always dynamic
export const dynamic = 'force-static';  // Always static
export const dynamic = 'auto';          // Default - auto detect

On-Demand Revalidation

// Tag-based revalidation
async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { tags: ['posts', `post-${slug}`] },
  });
  return res.json();
}

// Revalidate via Server Action or Route Handler
// app/actions.ts
'use server';
import { revalidateTag, revalidatePath } from 'next/cache';

export async function updatePost(slug: string) {
  // Update in database...
  
  // Revalidate specific tag
  revalidateTag(`post-${slug}`);
  
  // Or revalidate all posts
  revalidateTag('posts');
  
  // Or revalidate a specific path
  revalidatePath('/posts');
  revalidatePath(`/posts/${slug}`);
}

// Route Handler for webhooks
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const { tag, secret } = await request.json();
  
  if (secret !== process.env.REVALIDATION_SECRET) {
    return Response.json({ error: 'Invalid secret' }, { status: 401 });
  }
  
  revalidateTag(tag);
  return Response.json({ revalidated: true });
}

Parallel Data Fetching

// ✅ GOOD: Fetch in parallel
async function getData() {
  // Start all fetches at the same time
  const [users, posts, comments] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json()),
  ]);
  
  return { users, posts, comments };
}

// ❌ BAD: Sequential fetching (waterfall)
async function getDataSlow() {
  const users = await fetch('/api/users').then(r => r.json());
  const posts = await fetch('/api/posts').then(r => r.json());
  const comments = await fetch('/api/comments').then(r => r.json());
  // Each waits for the previous one!
}

Data Fetching Patterns

// Pattern 1: Fetch at component level (recommended)
// Each component fetches its own data
async function UserProfile({ userId }: { userId: string }) {
  const user = await getUser(userId);
  return <div>{user.name}</div>;
}

async function UserPosts({ userId }: { userId: string }) {
  const posts = await getUserPosts(userId);
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}

// Next.js automatically deduplicates identical requests!
// If both components fetch the same user, only 1 request is made

// Pattern 2: Preload pattern for waterfalls
import { preload } from './data';

export default async function Page({ params }: { params: { id: string } }) {
  // Start fetching early
  preload(params.id);
  
  // Do other work...
  
  // Data is already being fetched
  const data = await getData(params.id);
}

Using Databases Directly

// In Server Components, access databases directly
// No API routes needed!

import { prisma } from '@/lib/prisma';
import { sql } from '@vercel/postgres';

// Using Prisma
async function getUsers() {
  const users = await prisma.user.findMany({
    where: { active: true },
    include: { posts: true },
  });
  return users;
}

// Using raw SQL
async function getProducts() {
  const { rows } = await sql`
    SELECT * FROM products 
    WHERE stock > 0 
    ORDER BY created_at DESC
  `;
  return rows;
}

// Using Drizzle
import { db } from '@/lib/drizzle';
import { users } from '@/lib/schema';

async function getAllUsers() {
  return db.select().from(users);
}

Loading States with Suspense

import { Suspense } from 'react';

// Slow component that fetches data
async function SlowComponent() {
  const data = await fetchSlowData(); // Takes 3 seconds
  return <div>{data}</div>;
}

// Fast component  
async function FastComponent() {
  const data = await fetchFastData(); // Takes 100ms
  return <div>{data}</div>;
}

// Page with streaming
export default function Page() {
  return (
    <div>
      {/* Shows immediately */}
      <h1>Dashboard</h1>
      
      {/* Fast component loads first */}
      <Suspense fallback={<p>Loading fast...</p>}>
        <FastComponent />
      </Suspense>
      
      {/* Slow component streams in later */}
      <Suspense fallback={<p>Loading slow...</p>}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

📖 Data Fetching Documentation →

✅ Data Fetching Best Practices

  • • Fetch data at the component level, not at the top
  • • Use parallel fetching with Promise.all()
  • • Leverage automatic request deduplication
  • • Use Suspense for loading states
  • • Choose appropriate caching strategy per data type
  • • Use tags for granular revalidation control