Skip to the solution
MediumFrontend6 min read

How do you interact with external APIs in React?

553 views

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.

  • api
  • data fetching
  • useeffect
  • axios

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactA small API client with error normalisation, and the hook that consumes it
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 56 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

How do you manage global state in a large React application?

Continue