Next.js Data Fetching — fetch, cache, revalidate

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Next.js Data Fetching — fetch, cache, revalidate

Next.js extends the Fetch API with powerful caching and revalidation features, giving you fine-grained control over data freshness.

The Fetch API with Next.js

Next.js extends fetch with built-in caching:

// app/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // Cache for 1 hour
  })
  return res.json()
}

export default async function Page() {
  const posts = await getPosts()
  return <div>{posts.map(p => <h2 key={p.id}>{p.title}</h2>)}</div>
}

Caching Strategies

Static Caching (Default)

// Cached by default until manually revalidated
async function getStaticData() {
  const res = await fetch('https://api.example.com/config')
  return res.json()
}

Time-Based Revalidation

// Cache for 60 seconds
async function getFreshPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 }
  })
  return res.json()
}

No Caching

// Fetch fresh on every request
async function getRealTimeData() {
  const res = await fetch('https://api.example.com/live', {
    next: { revalidate: 0 }
  })
  return res.json()
}

On-Demand Revalidation

Revalidate specific data when needed using Server Actions:

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function updatePost(id: string, data: any) {
  // Update in database
  await db.posts.update({ where: { id }, data })

  // Revalidate the specific page
  revalidatePath(`/blog/${id}`)
}
// app/blog/[id]/edit/page.tsx
'use client'

import { updatePost } from '@/app/actions'

export function EditPostForm({ postId }: { postId: string }) {
  async function handleSubmit(formData: FormData) {
    await updatePost(postId, {
      title: formData.get('title'),
      content: formData.get('content')
    })
  }

  return (
    <form action={handleSubmit}>
      <input name="title" />
      <textarea name="content" />
      <button type="submit">Save</button>
    </form>
  )
}

Revalidate Tags

Use tags to revalidate related data:

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

async function getAuthor(authorId: string) {
  const res = await fetch(`https://api.example.com/authors/${authorId}`, {
    next: { tags: [`author-${authorId}`] }
  })
  return res.json()
}
// app/actions.ts
'use server'

import { revalidateTag } from 'next/cache'

export async function updateAuthorName(authorId: string, name: string) {
  await db.authors.update({ where: { id: authorId }, data: { name } })

  // Revalidate all posts by this author
  revalidateTag(`author-${authorId}`)
}

Advanced Fetching Patterns

Parallel Data Fetching

export default async function DashboardPage() {
  // Fetch in parallel
  const [users, posts, analytics] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/analytics').then(r => r.json())
  ])

  return (
    <div>
      <UserList users={users} />
      <PostList posts={posts} />
      <Analytics data={analytics} />
    </div>
  )
}

Sequential Data Fetching

export default async function PostPage({ params }) {
  // First fetch
  const post = await fetch(`/api/posts/${params.id}`).then(r => r.json())

  // Then fetch related data
  const author = await fetch(`/api/authors/${post.authorId}`).then(r => r.json())
  const comments = await fetch(`/api/posts/${params.id}/comments`).then(r => r.json())

  return (
    <article>
      <h1>{post.title}</h1>
      <p>By {author.name}</p>
      <Comments items={comments} />
    </article>
  )
}

Custom Fetch Wrapper

Create a reusable fetch wrapper:

// lib/fetch.ts
export async function fetchAPI(
  endpoint: string,
  options: {
    cache?: 'no-store' | 'force-cache'
    next?: { revalidate?: number; tags?: string[] }
  } = {}
) {
  const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'https://api.example.com'
  const url = `${baseUrl}${endpoint}`

  try {
    const res = await fetch(url, {
      headers: {
        'Authorization': `Bearer ${process.env.API_KEY}`
      },
      ...options
    })

    if (!res.ok) throw new Error(`API error: ${res.status}`)

    return res.json()
  } catch (error) {
    console.error(`Fetch failed for ${endpoint}:`, error)
    throw error
  }
}

Usage:

const data = await fetchAPI('/posts', {
  next: { revalidate: 60, tags: ['posts'] }
})

Error Handling and Fallbacks

export default async function Page() {
  try {
    const data = await fetch('/api/data', { next: { revalidate: 60 } })
      .then(r => {
        if (!r.ok) throw new Error('Failed to fetch')
        return r.json()
      })

    return <div>{/* render data */}</div>
  } catch (error) {
    return <div>Failed to load data. Please try again later.</div>
  }
}

Background Revalidation

Using ISR (Incremental Static Regeneration):

// app/blog/[slug]/page.tsx
export const revalidate = 60 // Revalidate every 60 seconds

export async function generateStaticParams() {
  const posts = await fetch('/api/posts').then(r => r.json())
  return posts.map(post => ({ slug: post.slug }))
}

export default async function PostPage({ params }) {
  const post = await fetch(`/api/posts/${params.slug}`).then(r => r.json())
  return <article>{post.content}</article>
}

Performance Monitoring

Track fetch performance:

async function fetchWithTiming(url: string) {
  const start = performance.now()
  const res = await fetch(url)
  const duration = performance.now() - start

  console.log(`Fetch ${url} took ${duration}ms`)

  return res.json()
}

FAQ

Q: What's the difference between revalidate and no-store? A: revalidate: 60 caches for 60 seconds then checks for updates. no-store never caches, always fetches fresh.

Q: Can I revalidate all pages at once? A: Use revalidateTag() with a broad tag, or revalidatePath('/', 'layout') to revalidate the entire app.

Q: How do I handle sensitive data like API keys in fetch? A: Use environment variables accessed only on the server, never exposed to the client. Next.js Server Components run on the server, so this is safe.


Master data fetching and caching for optimal performance and freshness.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro