Lesson 34 of 45 intermediate

API Integration with a .NET Backend

One typed client, three screen states, and a check that the server really said yes

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

A fetch call gets a 500 response from the API. What does the returned promise do?

Open interactive version (quiz + challenge)

Real-world analogy

Ordering from a restaurant by phone, you speak to one person, the waiter, instead of shouting to the kitchen. The waiter writes the order in a known format and tells you if it takes long. When the answer is "we are out of that", the waiter says so clearly. You do not treat the apology as a dish. If you change your order, the waiter cancels the old one first. The waiter is the API client module. The apology is an error status, and cancelling the old order is aborting a request.

What is it?

API integration is the layer between your React screens and the HTTP API. A good one has a single typed module. It builds requests, checks the status, turns error bodies into messages, and returns typed data. Screens call it from effects or a data library and render loading, error and data. The goal is that a failed request becomes a clear message, not a crash or a silent wrong value.

Real-world relevance

A product list page calls the .NET API for page two. The server returns 500 for a moment during a deploy. Without an ok check, the page tries to map over an error object and crashes. With the check, the user sees a retry message. On the form side, a 400 ProblemDetails has an errors object. The page can show "Name is required" under the right field. A generic failure message would hide that.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

The browser console shows a CORS error when your React app calls the API. Where is the fix?

Why call abort() in an effect cleanup that makes a request?

Code example

// api/products.ts  (a fragment: two files shown one after the other)
export type Product = { id: number; name: string; price: number };
export type Page<T> = { items: T[]; totalCount: number };

const baseUrl = import.meta.env.VITE_API_URL as string;

export async function getProducts(page: number, signal?: AbortSignal): Promise<Page<Product>> {
  const response = await fetch(baseUrl + '/api/products?page=' + page, { signal });
  if (!response.ok) {
    const problem = await response.json().catch(() => null);
    throw new Error(problem?.title ?? 'Request failed with ' + response.status);
  }
  return response.json() as Promise<Page<Product>>;
}

// ProductList.tsx  (assume useState and useEffect are imported)
export function ProductList({ page }: { page: number }) {
  const [data, setData] = useState<Page<Product> | null>(null);
  const [error, setError] = useState<string | null>(null);
  useEffect(() => {
    const controller = new AbortController();
    setData(null); setError(null);
    getProducts(page, controller.signal)
      .then(setData)
      .catch(e => {
        if (e.name !== 'AbortError') setError(e.message);
      });
    return () => controller.abort();
  }, [page]);

  if (error) return <p role="alert">{error}</p>;
  if (!data) return <p>Loading...</p>;
  return <ul>{data.items.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

Line-by-line walkthrough

  1. 1. This is a fragment of two files. The first block is the API module: types for a product and a page of products.
  2. 2. baseUrl comes from an environment variable, so the same code works in each environment.
  3. 3. getProducts checks response.ok. On failure it reads the ProblemDetails title if there is one and throws an Error.
  4. 4. On success it returns the JSON body typed as a Page of Product.
  5. 5. ProductList starts with no data and no error. Its effect creates an AbortController and calls the client with the signal.
  6. 6. The cleanup aborts the request when page changes or the component unmounts, and AbortError is ignored on purpose.
  7. 7. The render handles three states in order: error, loading, then the data.

Spot the bug

type Product = { id: number; name: string };

async function loadProducts(): Promise<Product[]> {
  const response = await fetch('/api/products');
  return response.json();
}

loadProducts()
  .then(products => console.log(products.length))
  .catch(() => console.log('failed'));
Need a hint?
What does the code do when the API answers with status 500 and a JSON error body?
Show answer
fetch resolves on a 500, so the error body is parsed and returned as if it were a product list. The catch never runs and products.length is undefined. Fix: check response.ok and throw before parsing.

Explain like I'm 5

You ask a helper to fetch something from a shop far away. The helper might come back with the thing or with a note saying the shop is closed. You must read the note, or you will try to eat a piece of paper.

Fun fact

Browsers send a preflight OPTIONS request before some cross-origin calls. If the API does not answer it correctly, the real request is never sent. The console then shows only a CORS error.

Hands-on challenge

Say it out loud in 30 seconds: "How do you call a .NET API safely from React?" Then check: did you mention a typed client module, checking response.ok, loading and error states, aborting on unmount, and ProblemDetails?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook