Back to Next.js tutorials
Intermediate14 min read

Fetching Data

Server-first data access patterns with fetch caching, ORM queries, and loading UI integration.

Server-First Data Access

Fetch in async Server Components as the default pattern. Direct database access via ORM (Prisma, Drizzle) eliminates HTTP overhead for internal data. External APIs use fetch with appropriate cache options.

Client-side fetching remains appropriate for user-specific live data after hydration, polling, and interactions that do not affect initial SEO — but default to server for initial page data.

  • ORM in Server Components for internal data
  • fetch for external APIs
  • Client fetch for post-hydration interactivity
export default async function Page() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 },
  });
  const data = await res.json();
  return <List items={data} />;
}

Fetch Cache Options

Next.js extends fetch with `cache: 'force-cache'` (default static), `cache: 'no-store'` (always fresh), and `next: { revalidate: seconds }` for ISR-style freshness. Tag fetches with `next: { tags: ['posts'] }` for on-demand invalidation.

Non-fetch async data (database direct) uses `unstable_cache` or route segment config `export const revalidate = 60` to control caching behavior at route level.

  • force-cache, no-store, revalidate options
  • Cache tags for targeted invalidation
  • Route segment revalidate for non-fetch data

Avoiding Waterfalls

Sequential awaits in one component create waterfalls — each waits for the previous. Parallelize independent fetches with Promise.all or split into sibling Server Components that fetch independently.

Preload patterns pass promises as props to child components wrapped in Suspense — parent starts fetch, child awaits same promise. React deduplicates identical in-flight requests.

  • Promise.all for parallel fetches
  • Sibling components fetch independently
  • Preload + Suspense for streaming

Data Shape and Performance

Select only columns and relations needed for the page — over-fetching slows queries and increases payload size. Paginate lists at the database, not in UI after fetching everything.

Transform data on the server before passing to Client Components — send view models, not raw ORM entities with circular references that break serialization.

  • Select minimal columns and relations
  • Server-side pagination
  • Serialize-friendly view models to client

Loading and Error States

Wrap slow server components in Suspense boundaries with loading.tsx fallbacks. Granular Suspense boundaries stream fast content while slow sections load — better than one page-level spinner.

Handle fetch errors with try/catch in server components, notFound() for 404 cases, or error.tsx for unexpected failures. Return typed error states when partial rendering is possible.

  • Granular Suspense boundaries
  • notFound() for missing resources
  • error.tsx for unexpected failures

Get In Touch


Ready to discuss your next project? Drop me a message.