async/await directly in Server Components, fetch() with Next.js cache extensions, loading skeletons with Suspense, and the redirect/notFound control-flow functions.
F-4 — Data Fetching in the App Router
Who this is for: Developers who understand the Server/Client Component boundary from F-3 and now want to know how to actually get data into their components. This module covers the practical mechanics — fetching data in Server Components, how Next.js extends
fetch(), where Suspense fits in, and the control-flow helpers that keep your code clean.
The Old Way vs The New Way
In the Pages Router, data fetching was a page-level concern. getServerSideProps, getStaticProps, getStaticPaths — all of these were special functions that ran at the page boundary and drilled data down as props. Your components were passive recipients. A <ProductReviews> component couldn't fetch its own reviews — it had to receive them from the page that fetched everything.
In the App Router, every Server Component can fetch its own data. There's no special function, no getServerSideProps, no props drilling required. A <ProductReviews> component that needs reviews just queries for them directly:
No useEffect. No loading state. No useState. The component is async, it awaits its data, and it renders. Next.js handles the rest.
Fetching with fetch() — Next.js Extensions
Next.js extends the native fetch() API with additional options that control caching and revalidation behaviour. These only apply to fetch() calls inside Server Components and Route Handlers — they don't affect client-side fetches.
Static (Cached Forever)
In Next.js 14, fetch was cached by default. This changed in Next.js 15.
Dynamic (No Cache — Always Fresh)
Use no-store for data that must be current at request time: inventory counts, user-specific content, real-time prices.
Time-Based Revalidation
This is ISR at the fetch level. The first request uses the cached data. After 60 seconds, the next request triggers a background re-fetch. The user still gets a fast response from cache; the fresh data is stored for subsequent requests.
Tag-Based Revalidation
Tag-based revalidation lets you invalidate specific cached data on demand — when a product is updated, for instance, you call revalidateTag('products') and the next request for any fetch tagged with 'products' will re-fetch. This is how you build CMS integrations where publishing content immediately updates the live site.
Next.js 15 Changed the Default
This is a migration trap worth understanding explicitly. In Next.js 14, fetch() was cached by default. In Next.js 15, fetch() is uncached by default — it behaves like cache: 'no-store' unless you explicitly opt in.
If you're upgrading a Next.js 14 project to 15, every fetch() call without an explicit cache option just became dynamic. Audit your data fetching on upgrade — your build output (the ○/λ symbols from F-1) will tell you if pages that should be static are now rendering dynamically.
Direct Database Access (No fetch() Required)
fetch() caching only applies to HTTP calls. When you're querying a database directly with Prisma, Drizzle, or raw SQL, you're not using fetch() at all — you're using a database client. In this case, Next.js's fetch() cache extensions don't apply.
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion