Skip to content

Data Fetching

Data fetching in the App Router is built around one idea: Server Components can await data directly. This lesson covers the fundamental patterns — sequential vs. parallel fetching, and where to put fetching logic.

Fetching Directly in Server Components

Because Server Components can be async, the simplest and most common data-fetching pattern in Next.js is calling await fetch(...) (or a database query) directly inside the component body. There's no useEffect, no loading state variable, and no client-side request — the data is already part of the HTML by the time it reaches the browser.

You can fetch data in any Server Component, not just top-level pages — a deeply nested component can fetch its own data independently, and Next.js automatically deduplicates identical fetch() calls made during the same render pass.

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch("https://api.example.com/posts");
  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: any) => <li key={post.id}>{post.title}</li>)}
    </ul>
  );
}

This is a complete, working data-fetching pattern — no extra library or client-side state management required.

Sequential vs. Parallel Fetching

// Sequential (slower — each await waits for the previous one)
const user = await getUser(id);
const posts = await getPostsByUser(id);

// Parallel (faster — both requests start at the same time)
const [user, posts] = await Promise.all([
  getUser(id),
  getPostsByUser(id),
]);
  • Sequential await calls are appropriate when one request depends on the result of another.
  • Independent requests should use Promise.all() to run in parallel and reduce total wait time.
  • Next.js does not automatically parallelize sequential await calls — you must do it explicitly.
  • Fetching can happen in any Server Component, not only in the top-level page.

Data Fetching Cheat Sheet

Core patterns and where to use each one.

Pattern When to Use
await fetch() in a Server Component Default choice for most data needs
Promise.all([...]) Multiple independent requests
Sequential await calls One request depends on another's result
Fetching in a nested component Data only needed by a specific part of the tree
Client-side fetching (useEffect/SWR) Data that changes based on client-only interaction

Colocating Data Fetching with the Component That Needs It

Because any Server Component can fetch its own data, you're not forced to fetch everything at the top of a page and pass it down through many layers of props. A deeply nested component can call its own getComments() function directly, keeping data requirements colocated with the UI that uses them.

Next.js deduplicates identical fetch() requests (same URL and options) made during a single render pass, so multiple components fetching the same resource does not result in duplicate network requests.

Fetching from a Database vs. an External API

Server Components aren't limited to fetch() — since their code runs only on the server, they can query a database directly using an ORM like Prisma or Drizzle, which is often simpler and faster than round-tripping through your own API endpoint.

// app/posts/page.tsx
import { db } from "@/lib/db";

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

There's no need for a separate /api/posts endpoint just to read data inside your own Server Components.

Common Mistakes

  • Fetching data with useEffect inside a Client Component when a Server Component could fetch it directly.
  • Writing several independent await calls sequentially instead of using Promise.all().
  • Building an internal API route just to read your own database from your own Server Components.
  • Not handling fetch failures, leaving users with an unhandled error instead of a friendly message.

Key Takeaways

  • Server Components can await data directly — no useEffect or loading state needed for initial data.
  • Use Promise.all() for independent requests to avoid unnecessary sequential waiting.
  • Data fetching can be colocated with any component that needs it, not just top-level pages.
  • Server Components can query databases directly without a separate API layer.
  • Next.js deduplicates identical fetch() calls within the same render pass automatically.

Pro Tip

Before writing a data-fetching function, ask whether the component that needs the data could just fetch it itself instead of receiving it via props from a parent several levels up — colocating fetching logic usually leads to simpler, more maintainable components.