The previous lesson, Two kinds of components, drew the line between server and client components, and one half of that line matters most here.
A server component can read a database or secret environment variables directly, and it ships zero JavaScript to the browser. Server components run only on the server, so they can safely touch credentials, and they contribute finished HTML without adding anything to the bundle. Hooks and event handlers belong to client components instead.
That first capability is what makes this unit possible. Data fetching in Next.js is mostly just a server component doing what a backend handler always did.
Just await it
The data-fetching dance from the React course took four moving parts: a useEffect, a loading flag, an error flag, and a state setter. In a server component the whole ceremony collapses into a single await:
// app/blog/page.js export default async function BlogPage() { const res = await fetch("https://api.example.com/posts"); const posts = await res.json(); return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); }
Decode it:
- The component itself is
async. Server components may be, client components may not. - There is no
useEffectand no loading state variables. The server simply waits for the data, renders, and sends finished HTML. - Because this runs on the server, the API call could just as well be a direct database query, like the ones you wrote in Backend with Node.js.
Skip the HTTP hop to your own API
A server component already runs on the server, so calling your own API over HTTP from inside it is a wasted round trip. The request would leave your server just to come back in through the front door. Query the data source directly instead:
// app/blog/page.js import { db } from "../lib/db"; export default async function BlogPage() { const posts = await db.query("SELECT id, title FROM posts ORDER BY created_at DESC"); return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); }
Decode it:
- This is the exact query code you wrote in Express handlers in Backend with Node.js, now living inside the component that renders the result.
- It is safe because server components never ship their code to the browser (Unit 3), so the connection string stays secret.
- Keep
fetchfor external APIs, meaning someone else's server. For your own database, query directly.
Fetch where you need it
The React course raised a familiar worry: fetch at the top and drill props down, or fetch deeper in the tree. In server components the recommendation is clear, fetch inside the component that needs the data.
That works because Next.js memoizes identical fetch calls during one render pass. If three components on the page request the same URL, only one real network request happens and all three receive its result.
That memoization lasts for a single render of a single request. It is not a cache that survives between visitors, which is the subject of the next lesson.
Reading your own Postgres from a server component
When a server component needs data from your own Postgres database, the recommended approach is to query the database directly with await inside the component.
The component already runs on the server, so fetching your own API endpoint would only add a pointless HTTP round trip to yourself. A direct query is faster and simpler, and it is safe because the component's code never reaches the browser, so credentials stay on the server.
Three approaches that do not apply here are worth naming so they do not tempt you:
useEffectbelongs to client components and cannot appear in a server component at all.localStorageonly exists in the browser, so there is nothing for the server to read.fetchis the right tool for external services, not for a database you already have a client for.
Two components, one network request
Picture a single page render in which a Header component and a Sidebar component both call fetch("https://api.example.com/user/7"). Exactly one real network request goes out.
Next.js deduplicates identical fetch calls inside one render pass, a behaviour called request memoization. The first call performs the request, and the second one receives the same in-flight result rather than starting a new one. Both components end up with identical data.
This is precisely why fetching locally in each component is the recommended style. You get the readability of colocated data fetching without paying for it in duplicate traffic.