beginner Step 4 of 15

Data Fetching

Next.js Development

Data Fetching

Data fetching is one of the most important aspects of Next.js and where it differs most from standard React. Next.js provides multiple strategies for fetching data: Server Components that fetch data directly on the server with async/await, the fetch API with built-in caching and revalidation, and the Pages Router's getServerSideProps and getStaticProps functions. Choosing the right data fetching strategy impacts your application's performance, SEO, and user experience. The App Router (Next.js 13+) simplifies data fetching dramatically by making components async by default.

Server Component Data Fetching (App Router)

In the App Router, components are Server Components by default. You can make them async and fetch data directly — no special functions needed. The data is fetched on the server and the rendered HTML is sent to the client.

// app/blog/page.tsx — Server Component (default)
interface Post {
  id: number;
  title: string;
  body: string;
}

async function getPosts(): Promise<Post[]> {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
    next: { revalidate: 3600 }, // Revalidate cache every hour
  });

  if (!res.ok) {
    throw new Error("Failed to fetch posts");
  }

  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <div>
      <h1>Blog Posts</h1>
      <div className="posts-grid">
        {posts.map((post) => (
          <article key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body.slice(0, 100)}...</p>
          </article>
        ))}
      </div>
    </div>
  );
}

Caching and Revalidation

Next.js extends the native fetch API with caching options. By default, fetch results are cached. You can control this with the cache and next.revalidate options.

// Cached indefinitely (default) — equivalent to getStaticProps
const data = await fetch("https://api.example.com/data");

// Revalidate every 60 seconds — ISR (Incremental Static Regeneration)
const data = await fetch("https://api.example.com/data", {
  next: { revalidate: 60 },
});

// No cache — equivalent to getServerSideProps
const data = await fetch("https://api.example.com/data", {
  cache: "no-store",
});

// Revalidate with tags — on-demand revalidation
const data = await fetch("https://api.example.com/posts", {
  next: { tags: ["posts"] },
});

// Trigger revalidation from an API route or Server Action:
// import { revalidateTag } from "next/cache";
// revalidateTag("posts");

Parallel and Sequential Data Fetching

// Parallel fetching — faster, fetches happen simultaneously
async function Dashboard() {
  // Start both fetches at the same time
  const [users, posts, stats] = await Promise.all([
    fetch("https://api.example.com/users").then((r) => r.json()),
    fetch("https://api.example.com/posts").then((r) => r.json()),
    fetch("https://api.example.com/stats").then((r) => r.json()),
  ]);

  return (
    <div>
      <UserList users={users} />
      <PostList posts={posts} />
      <StatsCard stats={stats} />
    </div>
  );
}

// Sequential fetching — when one fetch depends on another
async function UserPosts({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;

  // First fetch the user
  const user = await fetch(`https://api.example.com/users/${id}`).then(
    (r) => r.json()
  );

  // Then fetch their posts (depends on user data)
  const posts = await fetch(
    `https://api.example.com/users/${user.id}/posts`
  ).then((r) => r.json());

  return (
    <div>
      <h1>{user.name}'s Posts</h1>
      {posts.map((post: Post) => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  );
}

Pages Router Data Fetching (Legacy)

// pages/blog.tsx — Pages Router (getStaticProps)
export async function getStaticProps() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();

  return {
    props: { posts },
    revalidate: 3600, // ISR: regenerate every hour
  };
}

export default function BlogPage({ posts }) {
  return posts.map((post) => <article key={post.id}>{post.title}</article>);
}

// pages/dashboard.tsx — Pages Router (getServerSideProps)
export async function getServerSideProps(context) {
  const { req } = context;
  const token = req.cookies.token;

  const res = await fetch("https://api.example.com/dashboard", {
    headers: { Authorization: `Bearer ${token}` },
  });

  if (!res.ok) {
    return { redirect: { destination: "/login", permanent: false } };
  }

  return { props: { data: await res.json() } };
}
Tip: In the App Router, prefer fetching data in Server Components as close to where the data is used as possible. Next.js automatically deduplicates identical fetch requests within a single render, so you do not need to worry about multiple components fetching the same data.

Key Takeaways

  • Server Components can be async and fetch data directly — no special APIs needed.
  • Next.js extends fetch with caching: default cache, revalidate for ISR, no-store for dynamic data.
  • Use Promise.all for parallel data fetching when requests are independent.
  • Tag-based revalidation with revalidateTag enables on-demand cache invalidation.
  • The Pages Router uses getStaticProps (static) and getServerSideProps (dynamic) for data fetching.