Lesson 14 of 2222 min read

How to Fetch Data from an API in Next.js (Real Examples)

A practical, real-world guide to fetching API data in Next.js using async Server Components, with loading.tsx and error.tsx for graceful UX.

Author: CodersNexus

How to Fetch Data from an API in Next.js (Real Examples)

You've learned the theory of rendering strategies and caching — now it's time to put it into practice with a complete, realistic data-fetching workflow. Fetching data from an API is rarely just a single fetch() call; a production-quality implementation needs to gracefully handle the time it takes to load, and gracefully handle what happens if the request fails.

This lesson walks through fetching real API data inside async Server Components, then introduces two special files the App Router provides specifically for this purpose: loading.tsx, which automatically shows a loading UI while a page's data is being fetched, and error.tsx, which automatically catches and displays a fallback UI if that fetching (or rendering) throws an error — both without you needing to manually wire up loading spinners or try/catch blocks in every single component.

Learning Objectives

  • Fetch data from a real API inside an async Server Component.
  • Handle non-OK API responses and thrown errors appropriately.
  • Use loading.tsx to automatically show a loading UI during data fetching.
  • Use error.tsx to automatically catch and display a fallback UI on failure.
  • Understand how loading.tsx and error.tsx relate to React Suspense and error boundaries under the hood.

Core Definitions

  • Async Server Component: A Server Component function declared with the async keyword, allowing direct use of await inside the component body to fetch data.
  • loading.tsx: A special file that, when placed alongside a page.tsx, automatically renders as an instant loading UI while that page's data is being fetched.
  • error.tsx: A special file that automatically renders as a fallback UI if an error is thrown during rendering or data fetching within its corresponding route segment.
  • Suspense boundary: A React mechanism for showing fallback UI while a component tree is still loading; loading.tsx is implemented as a Suspense boundary automatically by Next.js.
  • Error boundary: A React mechanism for catching JavaScript errors in a component tree and displaying fallback UI instead of crashing the whole app; error.tsx is implemented as an error boundary automatically by Next.js.

Detailed Explanation

Fetching real API data in a Server Component is refreshingly direct: because Server Components can be declared async, you simply await your fetch call right inside the component body, no useEffect or loading state management required. If the API call fails — the network request itself throws, or the response comes back with a non-OK status like 404 or 500 — that error naturally propagates up, which is exactly what error.tsx is designed to catch.

But what does a visitor see WHILE that await is resolving? Without any special handling, the entire page would simply not render until the fetch completes, leaving visitors staring at a blank browser tab or the previous page for however long the request takes. This is where loading.tsx comes in: simply creating a loading.tsx file in the same folder as a page.tsx tells Next.js to automatically show that loading UI immediately, then swap in the real page content the moment its data finishes loading — all without you writing any manual isLoading state or conditional rendering logic yourself.

Under the hood, loading.tsx works by automatically wrapping your page in a React Suspense boundary. When the async page component suspends (because it's awaiting data), React shows the Suspense fallback — your loading.tsx content — and swaps to the real content once the promise resolves. This is a meaningful upgrade over manually managing loading state, since it happens automatically for any async component within that route, and even supports showing different loading states for different nested parts of a page as they each individually finish loading (a pattern called streaming, which composes naturally with nested loading.tsx files at different folder levels).

error.tsx works similarly but for the failure case: it must be a Client Component (requiring 'use client', since error boundaries rely on React features only available client-side) and automatically wraps your route in an error boundary. If the page component throws — whether from a failed fetch, a thrown exception from a non-OK response, or any other rendering error — Next.js automatically renders your error.tsx content instead of letting the error crash the whole application. error.tsx components receive an error prop (the actual error object) and a reset function you can call to attempt re-rendering the segment again, commonly wired up to a 'Try Again' button.

Together, page.tsx (the happy path), loading.tsx (the in-progress state), and error.tsx (the failure state) give you a complete, production-quality data-fetching UX for any route, using conventions rather than manual state management scattered throughout your components.

Diagram Description

Visualize the three states a data-fetching route can be in, and which file handles each:

app/products/
├── page.tsx → happy path: awaits fetch, renders the actual product list
├── loading.tsx → shown INSTANTLY while page.tsx's fetch is still pending
└── error.tsx → shown automatically if page.tsx's fetch throws or returns a bad response

Timeline for a visitor:
[Visit /products] --> [loading.tsx shown immediately] --> [fetch resolves]
--> SUCCESS: page.tsx renders with real data
--> FAILURE: error.tsx renders instead, with a 'Try Again' button calling reset()

Comparison Table

FileHandlesComponent TypeMechanism
page.tsxSuccessful data fetch and renderingServer Component (can be async)Direct await inside the component
loading.tsxThe in-progress/pending stateServer or Client ComponentAutomatic React Suspense boundary
error.tsxThrown errors during fetch or renderMust be a Client Component ('use client')Automatic React error boundary

Next.js Practical Example

// app/products/page.tsx — the happy path, fetches and renders real data
async function getProducts() {
  const res = await fetch('https://api.example.com/products');
  if (!res.ok) {
    throw new Error('Failed to fetch products'); // caught automatically by error.tsx
  }
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts();
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name} — ₹{p.price}</li>
      ))}
    </ul>
  );
}

// app/products/loading.tsx — shown instantly while the fetch above is pending
export default function Loading() {
  return <p>Loading products…</p>;
}

// app/products/error.tsx — shown automatically if the fetch above throws
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <p>Something went wrong: {error.message}</p>
      <button onClick={() => reset()}>Try Again</button>
    </div>
  );
}

getProducts explicitly checks res.ok and throws an Error if the API call didn't succeed — this thrown error automatically propagates up and is caught by the nearest error.tsx boundary rather than crashing the app. While ProductsPage's await is pending, Next.js automatically displays Loading (from loading.tsx) instantly, requiring zero manual state management in ProductsPage itself. If the fetch ultimately fails, Next.js automatically swaps in the Error component (from error.tsx) instead of ProductsPage's content, passing it the actual error object plus a reset function; clicking the 'Try Again' button calls reset(), which attempts to re-render the segment, effectively retrying the fetch.

Industry Examples

  • E-commerce product listing pages use loading.tsx to show skeleton screens while product data fetches, and error.tsx to show a friendly 'couldn't load products, try again' message if the API is temporarily down.
  • Dashboard applications use nested loading.tsx files at different route levels to stream in different sections of a complex dashboard independently, rather than blocking the entire page on the slowest data source.
  • News sites use error.tsx to gracefully handle cases where a specific article's API call fails, showing a 'this article is temporarily unavailable' message instead of crashing the entire site.
  • SaaS billing pages use loading.tsx to show an instant skeleton for invoice history while the actual billing API call completes, improving perceived performance on a data-heavy page.
  • Travel booking sites use error.tsx to catch failures from third-party flight/hotel search APIs, showing a retry button so users aren't stuck on a broken page during high-traffic search spikes.

Interview Questions and Answers

Q1. How do you fetch data inside a Next.js Server Component?

By declaring the component function as async and using await directly inside its body to call fetch() or any other async data source, then using the resolved data directly in the returned JSX — no useEffect or manual loading state is required.

Q2. What is the purpose of loading.tsx and how does it work under the hood?

loading.tsx provides an instant fallback UI shown automatically while a page's data is still being fetched. Next.js implements this by automatically wrapping the page in a React Suspense boundary, showing loading.tsx as the Suspense fallback until the async component's data resolves.

Q3. What is the purpose of error.tsx and what props does it receive?

error.tsx provides a fallback UI automatically shown if an error is thrown during rendering or data fetching within its route segment. It receives an error prop (the thrown error object) and a reset function that can be called to attempt re-rendering the segment again.

Q4. Why must error.tsx be a Client Component?

Because error.tsx is implemented as a React error boundary, and error boundaries rely on React class component lifecycle behavior and client-side error handling mechanisms that require the 'use client' directive to function correctly.

Q5. What happens if a fetch call inside a Server Component returns a non-OK HTTP status, like 404 or 500?

fetch() itself does not automatically throw for non-OK statuses — it only rejects on network failures. To trigger error.tsx's fallback UI for a bad response, you must explicitly check response.ok and throw an error yourself if it's false.

MCQs With Answers

1. How do you fetch data directly inside a Server Component?

  1. Using useEffect and useState
  2. Declaring the component async and using await directly
  3. Using a special Next.js-only data hook
  4. Fetching is not possible in Server Components

Answer: B. Declaring the component async and using await directly

Explanation: Async Server Components can use await directly in their function body to fetch data, without needing useEffect or manual loading state.

2. What does loading.tsx do when placed alongside a page.tsx?

  1. It permanently replaces the page
  2. It shows automatically while that page's data is being fetched
  3. It only works in Client Components
  4. It has no effect unless manually imported

Answer: B. It shows automatically while that page's data is being fetched

Explanation: loading.tsx is automatically used as a Suspense fallback shown while the corresponding page's async data fetching is still in progress.

3. Why must error.tsx include the 'use client' directive?

  1. Because it needs useState for styling
  2. Because it's implemented as a React error boundary, which requires client-side behavior
  3. Because Next.js requires all special files to be Client Components
  4. It doesn't need 'use client'

Answer: B. Because it's implemented as a React error boundary, which requires client-side behavior

Explanation: Error boundaries rely on React mechanisms only available in Client Components, so error.tsx must include the 'use client' directive.

4. What does the reset function passed to error.tsx typically do?

  1. Reloads the entire browser page
  2. Attempts to re-render the affected route segment again
  3. Deletes cached data permanently
  4. Logs the user out

Answer: B. Attempts to re-render the affected route segment again

Explanation: Calling reset() attempts to re-render the segment that previously threw an error, commonly wired to a 'Try Again' button.

5. Does fetch() automatically throw an error for a 404 or 500 response?

  1. Yes, always
  2. No — you must explicitly check response.ok and throw manually
  3. Only in Client Components
  4. Only if using no-store

Answer: B. No — you must explicitly check response.ok and throw manually

Explanation: fetch() only rejects on network-level failures; a non-OK HTTP status like 404 or 500 must be explicitly checked and thrown as an error to trigger error.tsx's fallback.

Common Mistakes to Avoid

  • Assuming fetch() automatically throws for non-OK HTTP responses (like 404 or 500), when it actually only rejects on network failures — response.ok must be checked manually.
  • Forgetting to add 'use client' to error.tsx, which will cause it to fail since error boundaries require client-side behavior.
  • Not creating a loading.tsx file and leaving visitors staring at a blank page during data fetching instead of an instant loading indicator.
  • Manually implementing isLoading state with useState/useEffect in a Server Component pattern where loading.tsx would handle it automatically and more simply.
  • Expecting error.tsx to catch errors from sibling or parent route segments outside its own folder's scope, when it only catches errors within its own segment.

Interview Notes

  • Async Server Components fetch data directly via await in the component body, with no useEffect required.
  • loading.tsx automatically renders as a Suspense fallback while a page's data fetching is in progress.
  • error.tsx automatically renders as an error boundary fallback if rendering or data fetching throws; it must be a Client Component.
  • fetch() does not throw automatically for non-OK responses; check response.ok and throw manually to trigger error.tsx.
  • error.tsx receives an error object and a reset function for retrying the failed segment.

Key Takeaways

  • Real-world data fetching needs to account for three states: success, loading, and error — and Next.js provides dedicated file conventions for each.
  • Async Server Components make the 'happy path' of data fetching remarkably simple, with no manual loading state management.
  • loading.tsx and error.tsx eliminate huge amounts of boilerplate that would otherwise be manually rewritten in every data-fetching component.
  • Explicitly checking response.ok and throwing is a small but essential step to ensure failed API calls are actually caught by error.tsx.

Summary

Fetching real API data in the Next.js App Router starts with async Server Components, which let you await a fetch call directly inside the component body, no useEffect or manual loading state required. To handle the in-progress loading state gracefully, a loading.tsx file placed alongside a page.tsx automatically renders as an instant fallback UI, implemented under the hood as a React Suspense boundary. To handle failures gracefully, an error.tsx file, which must be a Client Component, automatically catches thrown errors as a React error boundary, receiving the error object and a reset function for retrying. Since fetch() doesn't automatically throw for non-OK HTTP responses, checking response.ok and throwing explicitly is essential to ensure failed requests are correctly caught by error.tsx. Together, page.tsx, loading.tsx, and error.tsx give you a complete, production-quality data-fetching experience using file conventions instead of scattered manual state management.

Frequently Asked Questions

Declare your page component as async, then use await directly inside it to call fetch() (or another async data source) and use the resolved data in your JSX, without needing useEffect or manual state management.

Without loading.tsx, visitors will simply see a blank or unchanged page while the async data fetch completes, with no automatic loading indicator. Adding loading.tsx gives them instant visual feedback instead.

No. Simply creating a loading.tsx file in the same folder as a page.tsx is enough — Next.js automatically wires it up as a Suspense fallback for that page's data fetching, with no imports or manual setup required.

Because fetch() doesn't automatically throw for non-OK HTTP statuses like 404 — it only rejects on network-level failures. You need to explicitly check response.ok in your data-fetching function and throw an Error yourself if the response wasn't successful.

Each loading.tsx or error.tsx applies to its own route segment (the folder it's placed in) and any nested routes beneath it that don't have their own more specific loading.tsx or error.tsx, so a single file can apply to multiple nested pages if placed at a shared parent folder level.

Calling reset() tells Next.js to attempt to re-render the affected route segment again, effectively retrying whatever caused the error (such as re-running the failed fetch call), commonly wired to a 'Try Again' button in the error UI.

It's the primary file-based convention for automatic error handling per route segment. You can also handle specific errors manually with try/catch inside a component for more granular control, but error.tsx provides a simple, consistent fallback for unhandled errors.

Yes, through a pattern called streaming: by placing loading.tsx files at different nested folder levels (or using React's Suspense component directly around specific parts of a page), different sections can show their own loading states and resolve independently rather than blocking the entire page.

No. Both are automatically detected and used by Next.js purely based on their file name and location alongside the corresponding page.tsx — no manual import or wiring is required.

error.tsx handles errors within its specific route segment and any nested routes without their own error.tsx. global-error.tsx is a special file that catches errors in the root layout itself, acting as a last-resort fallback for the entire application when even the root layout fails.