Next.js Data Fetching Cheat Sheet

Next.js Data Fetching Cheatsheet

Server-Side Fetching

Basic Server Fetching

// app/posts/page.js
async function getPosts() {
    const res = await fetch('https://api.example.com/posts', {
        next: { revalidate: 3600 } // Cache for 1 hour
    });
    
    if (!res.ok) {
        throw new Error('Failed to fetch posts');
    }
    
    return res.json();
}

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

Fetch with Error Handling

async function getData() {
    try {
        const res = await fetch('https://api.example.com/data', {
            next: { revalidate: 60 }
        });
        
        if (!res.ok) {
            throw new Error(`HTTP ${res.status}: ${res.statusText}`);
        }
        
        return await res.json();
    } catch (error) {
        console.error('Fetch error:', error);
        return { error: 'Failed to fetch data' };
    }
}

Multiple Data Sources

async function getPageData() {
    // Fetch in parallel
    const [posts, users, comments] = await Promise.all([
        fetch('https://api.example.com/posts').then(res => res.json()),
        fetch('https://api.example.com/users').then(res => res.json()),
        fetch('https://api.example.com/comments').then(res => res.json())
    ]);
    
    return { posts, users, comments };
}

Client-Side Fetching

Basic Client Fetching

// app/components/ClientData.js
'use client';

import { useState, useEffect } from 'react';

export default function ClientData() {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
        async function fetchData() {
            try {
                const res = await fetch('/api/data');
                const result = await res.json();
                setData(result);
            } catch (err) {
                setError(err.message);
            } finally {
                setLoading(false);
            }
        }
        
        fetchData();
    }, []);
    
    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error}</div>;
    
    return <div>{/* render data */}</div>;
}

SWR for Client Fetching

'use client';

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then(res => res.json());

export default function SWRComponent() {
    const { data, error, isLoading, mutate } = useSWR('/api/posts', fetcher, {
        refreshInterval: 5000, // Refresh every 5 seconds
        revalidateOnFocus: true
    });
    
    if (isLoading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;
    
    return (
        <div>
            {data?.map(post => (
                <div key={post.id}>{post.title}</div>
            ))}
            <button onClick={() => mutate()}>Refresh</button>
        </div>
    );
}

Static Site Generation (SSG)

getStaticProps (Pages Router)

// pages/posts/[id].js
export async function getStaticProps({ params }) {
    const res = await fetch(`https://api.example.com/posts/${params.id}`);
    const post = await res.json();
    
    return {
        props: {
            post
        },
        revalidate: 3600 // ISR: revalidate every hour
    };
}

export async function getStaticPaths() {
    const res = await fetch('https://api.example.com/posts');
    const posts = await res.json();
    
    const paths = posts.map(post => ({
        params: { id: post.id.toString() }
    }));
    
    return {
        paths,
        fallback: 'blocking' // or 'false' or 'true'
    };
}

App Router Static Generation

// app/posts/[id]/page.js
async function getPost(id) {
    const res = await fetch(`https://api.example.com/posts/${id}`, {
        next: { revalidate: 3600 }
    });
    return res.json();
}

export default async function PostPage({ params }) {
    const post = await getPost(params.id);
    
    return (
        <article>
            <h1>{post.title}</h1>
            <p>{post.content}</p>
        </article>
    );
}

Incremental Static Regeneration (ISR)

Time-Based Revalidation

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

export default async function ProductsPage() {
    const products = await getProducts();
    
    return (
        <div>
            <h1>Products</h1>
            {products.map(product => (
                <div key={product.id}>{product.name}</div>
            ))}
        </div>
    );
}

On-Demand Revalidation

// app/api/revalidate/route.js
import { revalidatePath, revalidateTag } from 'next/cache';

export async function POST(request) {
    const { path, tag } = await request.json();
    
    if (path) {
        revalidatePath(path);
    }
    
    if (tag) {
        revalidateTag(tag);
    }
    
    return Response.json({ revalidated: true });
}

// Usage: Trigger revalidation
await fetch('/api/revalidate', {
    method: 'POST',
    body: JSON.stringify({ path: '/products' })
});

API Routes

Basic API Route

// app/api/posts/route.js
export async function GET() {
    const posts = await getPostsFromDatabase();
    return Response.json(posts);
}

export async function POST(request) {
    const data = await request.json();
    const newPost = await createPost(data);
    return Response.json(newPost, { status: 201 });
}

API Route with Caching

// app/api/data/route.js
export const revalidate = 3600; // Cache for 1 hour

export async function GET() {
    const data = await fetch('https://external-api.com/data', {
        cache: 'force-cache'
    });
    
    return Response.json(await data.json());
}

Data Fetching Patterns

Parallel vs Sequential

// Parallel fetching (faster)
async function getParallelData() {
    const [users, posts, comments] = await Promise.all([
        fetch('/api/users').then(res => res.json()),
        fetch('/api/posts').then(res => res.json()),
        fetch('/api/comments').then(res => res.json())
    ]);
    
    return { users, posts, comments };
}

// Sequential fetching (when data depends on previous)
async function getSequentialData() {
    const user = await fetch('/api/user/1').then(res => res.json());
    const posts = await fetch(`/api/posts?userId=${user.id}`).then(res => res.json());
    const comments = await fetch(`/api/comments?postId=${posts[0].id}`).then(res => res.json());
    
    return { user, posts, comments };
}

Conditional Fetching

async function getConditionalData(userId) {
    if (!userId) {
        return { posts: [] };
    }
    
    const posts = await fetch(`/api/posts?userId=${userId}`, {
        next: { tags: ['posts', `user-${userId}`] }
    }).then(res => res.json());
    
    return { posts };
}

Error Handling

Server-Side Error Handling

async function getDataWithFallback() {
    try {
        const res = await fetch('https://api.example.com/data', {
            next: { revalidate: 3600 }
        });
        
        if (!res.ok) {
            throw new Error(`HTTP ${res.status}`);
        }
        
        return await res.json();
    } catch (error) {
        // Return fallback data
        return { error: true, message: 'Failed to fetch data' };
    }
}

Client-Side Error Handling

'use client';

import { useState, useEffect } from 'react';

export default function DataComponent() {
    const [state, setState] = useState({
        data: null,
        loading: true,
        error: null
    });
    
    useEffect(() => {
        async function fetchData() {
            try {
                setState(prev => ({ ...prev, loading: true }));
                const res = await fetch('/api/data');
                
                if (!res.ok) {
                    throw new Error(`HTTP ${res.status}`);
                }
                
                const data = await res.json();
                setState({ data, loading: false, error: null });
            } catch (error) {
                setState({ data: null, loading: false, error: error.message });
            }
        }
        
        fetchData();
    }, []);
    
    if (state.loading) return <div>Loading...</div>;
    if (state.error) return <div>Error: {state.error}</div>;
    
    return <div>{/* render state.data */}</div>;
}

Performance Optimization

Request Deduplication

// Next.js automatically deduplicates identical requests
async function getPosts() {
    // Multiple calls to this function will be deduplicated
    const res = await fetch('https://api.example.com/posts', {
        next: { revalidate: 3600 }
    });
    return res.json();
}

export default async function Page() {
    // These will be deduplicated
    const [posts1, posts2] = await Promise.all([
        getPosts(),
        getPosts()
    ]);
    
    return <div>{/* content */}</div>;
}

Streaming with Suspense

import { Suspense } from 'react';

async function SlowData() {
    await new Promise(resolve => setTimeout(resolve, 2000));
    const data = await fetch('https://api.example.com/slow-data');
    return data.json();
}

export default function Page() {
    return (
        <div>
            <h1>Fast Content</h1>
            <Suspense fallback={<div>Loading slow data...</div>}>
                <SlowData />
            </Suspense>
        </div>
    );
}

Best Practices

Cache Strategies

// Static data - cache for long periods
const staticData = await fetch('/api/static-data', {
    next: { revalidate: 86400 } // 24 hours
});

// Dynamic data - shorter cache or no cache
const dynamicData = await fetch('/api/dynamic-data', {
    cache: 'no-store'
});

// User-specific data - no cache
const userData = await fetch('/api/user-data', {
    cache: 'no-store',
    headers: { 'Authorization': `Bearer ${token}` }
});

Error Boundaries

// app/error.js
'use client';

export default function Error({ error, reset }) {
    return (
        <div>
            <h2>Something went wrong!</h2>
            <p>{error.message}</p>
            <button onClick={reset}>Try again</button>
        </div>
    );
}

Loading States

// app/loading.js
export default function Loading() {
    return (
        <div className="loading">
            <div className="spinner"></div>
            <p>Loading...</p>
        </div>
    );
}