The Next.js App Router Data-Fetching Patterns You Actually Need
A practical guide to fetching data in the App Router — what belongs in Server Components, when Server Actions make sense, and where client-side fetching still earns its place.
By NavTech Solution
Most confusion around App Router data fetching comes from treating it like the Pages Router with new syntax. It isn't. The App Router changes where data fetching happens by default, and getting that placement right matters more than which specific API you call.
Start every route on the server
By default, every component in the App Router is a Server Component, and Server Components can be async and fetch data directly:
async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await getProduct(id);
return <ProductDetail product={product} />;
}
No loading state, no useEffect, no client-side waterfall. The HTML that reaches the browser already contains the data. This should be your default for anything that doesn't need interactivity — which, on most pages, is most of the content.
The mistake we see most often: a "use client" directive added to an entire page just because one button needs an onClick handler. Push the client boundary down to the smallest component that actually needs it, and let everything above it stay a Server Component.
Server Actions are for mutations, not queries
Server Actions solve a real problem — calling server-side logic from a form or event handler without hand-rolling an API route — but they're for mutations, not for fetching data to render. If you're pulling data into a page to display it, that's a Server Component doing an await. If you're submitting a form, updating a record, or triggering a side effect, that's a Server Action.
async function updateProfile(formData: FormData) {
"use server";
const name = formData.parse(z.object({ name: z.string().min(1) }));
await db.user.update({ name });
revalidatePath("/profile");
}
Re-validate on the server even though the client already validated with Zod. Client validation is UX; server validation is the actual security boundary, since nothing stops a request from arriving without ever touching your form.
Caching is the part people get wrong
The App Router's data cache is powerful and, if you don't understand it, surprising. fetch requests are cached by default in production builds unless you explicitly opt out. That's great for a blog post that doesn't change every request and actively wrong for anything user-specific or frequently changing.
Three levers matter here:
fetch(url, { cache: "no-store" })for data that must always be fresh.fetch(url, { next: { revalidate: 60 } })for data that's fine being up to a minute stale.revalidateTag/revalidatePathto invalidate on demand after a mutation, rather than waiting for a time-based window.
Pick the caching strategy per request, not per project. A dashboard's account balance and its list of historical invoices don't need the same freshness guarantee.
Streaming fixes the waterfall you'd otherwise build by hand
If a page needs data from three different sources and one of them is slow, don't make the whole page wait. Wrap the slow part in <Suspense> and let the rest of the page render immediately:
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={id} />
</Suspense>
This is streaming HTML, not client-side loading spinners hiding a fully-blocked page. The static shell reaches the browser fast, and the slow section fills in as it resolves — which is a meaningfully different (and better) experience than what most teams build by hand with useEffect and loading booleans.
When client-side fetching still makes sense
Not everything belongs on the server. If data needs to refetch based on user interaction that doesn't warrant a full navigation — live search-as-you-type, a polling dashboard widget, infinite scroll — client-side fetching (with something like TanStack Query, if the complexity justifies the dependency) is still the right tool. The goal isn't "server-render everything," it's "server-render by default, and add client-side fetching deliberately where it solves a problem the server can't."
If you're weighing whether an existing Next.js codebase is using these patterns correctly, or whether a Pages Router migration is worth the effort, that's exactly the kind of audit we do as part of Next.js development engagements — and if the data model itself needs a rethink (multi-tenant apps in particular get this wrong early), see our note on designing SaaS data models before you need them.