Data Fetching Patterns in the Next.js App Router
A practical tour of data fetching in the App Router: async Server Components, caching defaults, parallel requests and when client-side fetching still earns its place.
The App Router quietly retired an entire generation of Next.js APIs. There is no getServerSideProps, no getStaticProps, and no special page-level data function at all. Instead, any Server Component can be async and fetch its own data. That sounds simpler, and mostly it is, but it also moves the important decisions elsewhere: where you fetch, what you cache, and how you avoid accidental request waterfalls.
Fetch where the data is used
The single biggest mental shift is that data fetching is no longer a page-level concern. A component deep in the tree can await its own data, and Next.js will render it on the server without shipping any of that logic to the browser. In practice this means you stop threading props through six layers of components and instead colocate the query with the component that renders it.
export default async function ProjectList() {
const res = await fetch('https://api.example.com/projects', {
next: { revalidate: 3600 },
});
const projects: Project[] = await res.json();
return (
<ul>
{projects.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}If you talk to a database rather than an HTTP API, the same principle applies: call your query function directly inside the Server Component. There is no need to route everything through an internal API just so the frontend can reach it.
Know the caching defaults
Since Next.js 15, fetch requests are no longer cached by default. That is a healthier default than the old cache-everything behaviour, but it means caching is now an explicit decision. You have three broad options for each request:
- Leave it uncached for genuinely per-request data such as personalised content
- Set next: { revalidate: n } for data that can be a few minutes or hours stale
- Add next: { tags: [...] } and invalidate with revalidateTag when content changes
Tag-based invalidation is usually the right answer for CMS-driven content, because it keeps pages static until an editor actually publishes something. Time-based revalidation suits data that changes on its own schedule, such as exchange rates or feeds you do not control.
Parallel beats sequential
The most common performance bug we find in App Router codebases is the accidental waterfall: two awaits in a row where neither request depends on the other. Each await blocks the next, so the response times add up instead of overlapping. The fix is old-fashioned Promise.all.
export default async function Dashboard() {
const [user, invoices] = await Promise.all([
getUser(),
getInvoices(),
]);
return <DashboardView user={user} invoices={invoices} />;
}For requests that genuinely depend on each other, consider whether a Suspense boundary lets you stream the slow part instead of blocking the whole page on it.
Request memoisation saves you from prop drilling
Next.js deduplicates identical fetch calls within a single render pass, and React's cache function does the same for direct database queries. This means two components can each call getUser() during the same request and only one query runs. It is fine, and often cleaner, for sibling components to request the same data independently rather than lifting it to a shared parent.
When to fetch on the client
Client-side fetching still has a place: data that changes after the page loads, polling, infinite scroll, or anything driven by user interaction. Libraries such as SWR or TanStack Query remain excellent here. The rule of thumb is simple: fetch on the server for the initial render, fetch on the client for everything that happens afterwards.
Colocation is the App Router's real gift: the component that renders the data owns the request for it.
None of these patterns is complicated on its own. The skill is choosing deliberately for each piece of data rather than letting defaults decide for you.
If your team is wrestling with App Router data flows, STRCLI helps agencies and product teams get Next.js architecture right — get in touch.