App Router Data Fetching 🍱
Next.js App Router treats data as a first-class citizen. Server components can await data directly, while built-in routes manage loading, errors, and not-found states.
Server Component Fetching 🧑🍳
// app/dashboard/page.tsx
async function getStats() {
const res = await fetch('https://api.example.com/stats', { cache: 'no-store' });
if (!res.ok) throw new Error('Failed');
return res.json();
}
export default async function Dashboard() {
const stats = await getStats();
return <StatsGrid stats={stats} />;
}- You can
awaitinside server components because they run on the server. - Functions can live in the same file or be imported utilities.
- Fetching server-side keeps secrets (API keys) safe and reduces bundle size.
fetch() Semantics ⚙️
Next.js wraps fetch to enable auto caching.
| Option | Behavior |
|---|---|
| default | Cached per request URL + headers; reused across renders |
{ cache: 'no-store' } | Always revalidate (dynamic) |
{ next: { revalidate: 60 } } | ISR-like caching; revalidate every 60s |
{ next: { tags: ['posts'] } } | Tag-based invalidation with revalidateTag('posts') |
- Identical fetch calls in the same request dedupe automatically.
- Use headers/cookies in fetch options to scope caching per user.
Streaming + Suspense 🌊
import { Suspense } from 'react';
const Feed = async () => {
const posts = await getPosts();
return <PostList posts={posts} />;
};
export default function Page() {
return (
<Suspense fallback={<Skeleton />}>
{/* Feed renders when data arrives */}
<Feed />
</Suspense>
);
}- Server components inside
Suspensestart streaming HTML as soon as they resolve. - Compose multiple Suspense boundaries for granular skeletons.
loading.tsx ⏳
- Place
app/dashboard/loading.tsxto show UI while the route (or nested segment) is loading. - Automatically displayed until the server segment resolves.
export default function Loading() {
return <SkeletonGrid />;
}error.tsx 🚨
app/dashboard/error.tsxcatches errors thrown in the matching segment.- Must be a client component (
'use client') because it can offer retry buttons.
'use client';
export default function Error({ error, reset }) {
return (
<div>
<p>Oops: {error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}not-found.tsx 🚫
- Rendered when
notFound()is called or data fetch returns a 404-like state. - Place
not-found.tsxalongsidepage.tsxto customize.
import { notFound } from 'next/navigation';
if (!product) notFound();not-found.tsx might show a search box or helpful links.
Debugging Tips 🧭
- Use
next dev --turboto view fetch logs (cache hits/misses). - Check the terminal output after
next build—Next.js reports which routes are static vs dynamic. - Use
export const revalidate = 0;for entire route segments that must always be dynamic.
Analogy: App Router data fetching is a conveyor belt—fetch results arrive on server trays,
loading.tsxshows placeholder plates,error.tsxhandles dropped trays, andnot-found.tsxpolitely tells guests when an item is out of stock.
Last updated on