Fetching data in Server Components
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
// 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
// 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:
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
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.
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.