Server-side data fetching is the default and most efficient way to load data in an App Router app.
In the App Router you fetch data where you need it: make a Server Component async and await the data, from fetch, an ORM or a database client.
export default async function Page() {
const posts = await db.post.findMany()
return <PostList posts={posts} />
}No useEffect, no loading flags, no API route in between, and secrets stay on the server.
Things to know:
fetchresults are not cached by default; uncached data is fetched at request time. Caching is opt-in with'use cache'(covered later).- Identical
fetchcalls in one render are memoised, so several components can request the same URL without duplicate requests. For database calls, wrap the function with React'scache()to get the same per-request deduplication. - Sequential
awaits create waterfalls. Start independent requests together withPromise.all, or let separate components fetch their own data inside separate Suspense boundaries so they stream in parallel. - Handle failures: check
res.ok, and decide between rendering a message,notFound()or throwing to an error boundary.
Client-side fetching is still useful for data that changes after load or depends on client state; use a library like SWR or TanStack Query, or pass a promise from a Server Component and read it with use().
Example
// lib/data.ts
import { cache } from 'react'
import { db } from '@/lib/db'
export const getUser = cache(async (id: string) => db.user.find(id)) // deduped per request
// app/users/[id]/page.tsx
import { getUser } from '@/lib/data'
async function getPosts(userId: string) {
const res = await fetch(`https://api.example.com/users/${userId}/posts`)
if (!res.ok) throw new Error('Failed to load posts')
return (await res.json()) as { id: number; title: string }[]
}
export default async function UserPage({ params }: PageProps<'/users/[id]'>) {
const { id } = await params
// start both requests at once instead of awaiting one after the other
const [user, posts] = await Promise.all([getUser(id), getPosts(id)])
return (
<section>
<h1>{user.name}</h1>
<ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
</section>
)
}Remember
- Make Server Components async and await data directly
- fetch is uncached by default; identical fetches in a render are memoised
- Use
Promise.allor separate Suspense boundaries to avoid waterfalls;cache()dedupes DB calls
Watch out for
- Awaiting independent requests one after another, so the page takes the sum of their durations.