Data fetching
Fetch data where it is needed, keep server data on the server when possible, and watch for accidental waterfalls.
Server Components can go straight to the source
An async Server Component can call fetch, an ORM, or a database function directly. You do not need to create an internal HTTP endpoint just to get data from your own server into your own Server Component.
In current App Router behaviour, fetch requests are not something you should assume are cached by default. Make reuse and freshness explicit.
export default async function Page() {
const projects = await db.query.projects.findMany()
return <ProjectList projects={projects} />
}Sequential awaits can quietly become a waterfall
If request B does not depend on request A, waiting for A before starting B only adds latency. Start independent work together.
This matters more as pages accumulate small service calls. Three harmless-looking awaits can turn into a noticeably slow route.
const [profile, projects, activity] = await Promise.all([
getProfile(),
getProjects(),
getActivity(),
])Colocate reads with the component that needs them
A giant page-level data loader is not automatically cleaner. Server Components make it practical for a section to own the query it needs.
Colocation works best when shared data access lives in reusable server functions, so the component owns the decision to fetch without duplicating domain logic.
Separate components do not justify duplicate expensive work
If multiple parts of a render need the same expensive computation or database result, make reuse deliberate. The exact mechanism depends on whether you need request-local deduplication or persistent caching.
Do not add a global cache merely because two components happen to call the same function. First decide the lifetime and invalidation semantics you actually need.
Can you explain “Data fetching” without reaching for the API reference?
Describe the main trade-off, one failure mode, and the decision you would make in a real project.