~/TechPurAI
~/tutorials/nextjs-from-scratch/fetching-data-in-server-components
beginner·part 5 of 22·2 min read

Fetching data in Server Components

Updated Aug 16, 2026JavaScript · Next.js

Every page so far has rendered hardcoded JSX. A Server Component can be async directly — no useEffect, no loading state managed by hand — since it only ever runs on the server, once, before any HTML reaches the browser.

A simple data layer

ts
// lib/posts.ts
export interface Post {
  slug: string;
  title: string;
  excerpt: string;
  content: string;
  date: string;
}

const posts: Post[] = [
  {
    slug: "hello-app-router",
    title: "Hello, App Router",
    excerpt: "The first post on devnotes.",
    content: "Full post content goes here.",
    date: "2026-08-01",
  },
  {
    slug: "server-components-explained",
    title: "Server Components, explained",
    excerpt: "What actually runs where, and why it matters.",
    content: "Full post content goes here.",
    date: "2026-08-05",
  },
];

export function getAllPosts(): Post[] {
  return posts;
}

A plain in-memory array stands in for a real database throughout this series — the pattern that follows is identical whether getAllPosts() reads from an array, a file, or a real query; only this one function's internals would change.

Making the page async

tsx
// app/posts/page.tsx
import Link from "next/link";
import { getAllPosts } from "@/lib/posts";

export default function PostsPage() {
  const posts = getAllPosts();
  return (
    <div>
      <h1>All posts</h1>
      {posts.map((post) => (
        <article key={post.slug}>
          <h2><Link href={`/posts/${post.slug}`}>{post.title}</Link></h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

getAllPosts() here is synchronous, so nothing needs async yet — but a real data source (a database call, an external API) is almost always asynchronous, and a Server Component handles that with plain await, directly in the component body:

tsx
export default async function PostsPage() {
  const posts = await getAllPosts();
  // ...
}

No useEffect, no useState for loading, no client-side request waterfall — the component simply doesn't render until the await resolves, entirely on the server.

Fetching from an external source

tsx
async function getRemotePosts() {
  const res = await fetch("https://api.example.com/posts", {
    next: { revalidate: 3600 },
  });
  return res.json();
}

Next.js extends the standard fetch() with a next option — revalidate: 3600 caches the response and reuses it for up to an hour across every request, instead of hitting the external API fresh every single time a page renders. Part 17 covers exactly what this caching behavior means for how a page gets rendered overall.

Common mistake

Reaching for useEffect + fetch inside a Server Component the way it's done in a client-rendered React app. It technically won't work at all without "use client" — but even in a Client Component, it throws away the entire point of fetching on the server: the data ships already-rendered in the initial HTML instead of requiring a second round-trip from the browser after the page loads.

Next: turning /posts/hello-app-router into a real page — dynamic routes, and the params they hand a component.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous4. Layouts: shared UI across routesnext →6. Dynamic routes and generateStaticParams