Skip to solution
mediumFrontend

How do you interact with external APIs in React?

552 views
01

Understand the problem

Question presented to candidate: "How would you structure the code that talks to your backend? Not just the request — the whole integration."

What a strong answer should cover:

  • Layering is the actual answer: components → a data hook → an API client module → the network. Components should not know about URLs, headers, or transport.
  • The API client centralises the base URL, auth headers, JSON parsing, error normalisation, timeouts, and retries — so those exist once, not per call site.
  • fetch does not reject on HTTP errors. A 404 or 500 resolves with ok: false; you must check it or every failure looks like success.
  • Normalise errors into one shape so components handle failure uniformly.
  • Auth: attach the token in the client, refresh on 401, and never keep secrets in client-side environment variables — anything in the bundle is public.
  • Cancellation with AbortController, and retries only for idempotent requests with backoff.
  • The consuming layer: a query library, or a custom hook wrapping the client.
  • CORS is a browser-enforced server configuration, not something you fix in React.
  • GraphQL changes the shape (one endpoint, client-specified queries) but not the layering.

Clarifying questions expected:

  • "REST or GraphQL, and is there an existing client or generated types?"
  • "How is auth handled — cookies or a bearer token we manage?"
  • "Do we have a framework where this could be a server-side call instead?"

Code / implementation expected: Yes — a small API client with error normalisation, and a hook consuming it.

apidata fetchinguseeffectaxios
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes fetch and hooks. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. This doc is about integration architecture; <a href="PASTE_DATA_FETCHING_URL_

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

A small API client with error normalisation, and the hook that consumes it
Run Playground
import { useState, useEffect, useCallback } from "react";

// ── LAYER 1: the API client. Everything about transport lives here. ────────
class ApiError extends Error {
  constructor(message, status, body) {
    super(message);
    this.name = "ApiError";
    this.status = status;
    this.body = body;
  }
}

const BASE_URL = "/api";                 // one place per environment
let authToken = "demo-token-123";        // set at sign-in, not per component

async function apiRequest(path, { signal, timeoutMs = 8000, ...options } = {}) {
  // fetch has no timeout of its own, so compose one with AbortController.
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  signal?.addEventListener("abort", () => controller.abort());

  try {
    const res = await fetch(BASE_URL + path, {
      ...options,
      signal: controller.signal,
      headers: {
        "Content-Type": "application/json",
        Authorization: "Bearer " + authToken,
        ...options.headers,
      },
    });

    // THE GOTCHA: fetch resolves on 404 and 500. Without this check the error
    // page gets parsed and rendered as if it were valid data.
    if (!res.ok) {
      const body = await res.text().catch(() => "");
      throw new ApiError("Request failed with " + res.status, res.status, body);
    }
    return await res.json();
  } finally {
    clearTimeout(timer);
  }
}

// The typed surface components actually use — no URLs escape this module.
const api = {
  getUser: (id, opts) => apiRequest("/users/" + id, opts),
};

// ── LAYER 2: a hook giving components React-shaped state. ──────────────────
function useUser(id) {
  const [state, setState] = useState({ status: "idle", data: null, error: null });

  const load = useCallback((signal) => {
    setState({ status: "loading", data: null, error: null });
    api.getUser(id, { signal })
      .then((data) => setState({ status: "success", data, error: null }))
      .catch((e) => {
        if (e.name === "AbortError") return;      // deliberate, not a failure
        setState({ status: "error", data: null, error: e });
      });
  }, [id]);

  useEffect(() => {
    const c = new AbortController();
    load(c.signal);
    return () => c.abort();
  }, [load]);

  return { ...state, retry: () => load() };
}

// ── LAYER 3: the component. Knows nothing about HTTP. ─────────────────────
export default function App() {
  const [id, setId] = useState(1);
  const { status, data, error, retry } = useUser(id);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
      <p>
        {[1, 2].map((n) => (
          <button key={n} onClick={() => setId(n)} style={{ marginRight: 6 }}>user {n}</button>
        ))}
      </p>

      {status === "loading" && <p>loading…</p>}

      {status === "error" && (
        <div style={{ border: "1px solid crimson", borderRadius: 6, padding: 10 }}>
          {/* A normalised error means one predictable shape to branch on. */}
          <strong style={{ color: "crimson" }}>
            {error.status === 404 ? "That user does not exist." :
             error.status >= 500 ? "The server had a problem." :
             "Could not reach the server."}
          </strong>
          <p style={{ fontSize: 12, color: "#666", margin: "6px 0" }}>
            {error.name}: {error.message}
          </p>
          <button onClick={retry}>retry</button>
        </div>
      )}

      {status === "success" && <p>{JSON.stringify(data)}</p>}

      <p style={{ color: "#666", fontSize: 13 }}>
        This playground has no /api route, so every request fails — which is the
        point: the error is normalised into an ApiError with a status, and the
        component branches on that rather than on transport details.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 55 of 119 decoded in the React.js track. One more won't hurt.

Back to track