Build the mental model
A real integration doesn't just call an API and hope for 200 OK every time. It has to survive the messy middle: a request that gets rate limited, a token that expired, a server having a bad minute.
This exercise combines two skills from this course — retry-with-backoff and clear error communication — into one small, reusable piece of code: an API call wrapper.
The wrapper's job is to sit between your application and the raw API call, translating whatever comes back into a successful result or a message a non-technical user could actually understand. "Error: 429" means nothing to an end user; "We're a bit busy, trying again..." does.
| Status | Should the wrapper retry? |
|---|---|
| 429 / 500 | Yes — often temporary; wait and retry with backoff. |
| 401 / 403 | No — the request itself needs fixing, not repeating. |
| 404 | No — the resource isn't there; retrying won't create it. |
Always bound your retries
An unbounded retry loop against a service that's genuinely down can hang your application indefinitely and hammer a service that's already struggling. A maximum retry count protects both sides.
REQUEST HANDLING STATE MACHINE
------------------------------
LOADING
|
v
call mockApiCall() --> returns one status code
|
|-- 200 --> SUCCESS (show the data, done)
|-- 401 --> NEEDS AUTH (ask user to sign in again, done)
|-- 403 --> FORBIDDEN (explain no access, done)
|-- 404 --> NOT FOUND (explain missing item, done)
|
|-- 429 --> retries left?
| |-- yes --> wait (backoff) --> back to LOADING
| |-- no --> GIVE UP (ask user to retry later)
|
|-- 500 --> retries left?
|-- yes --> wait (backoff) --> back to LOADING
|-- no --> GIVE UP (ask user to retry later)Connect it to a real scenario
You'll build callWithHandling(mockApiCall) — a function that calls a pre-scripted mock API and returns a friendly outcome for every status it might see.
Build a deterministic mock
createMockApiCall(statusSequence) returns a function that, on each call, returns the next status code from a fixed array — no flaky network involved, so the outcome is exactly reproducible.
Handle terminal states immediately
On 200, return success right away. On 401, 403, or 404, return a specific friendly message immediately without retrying, since another attempt cannot change these outcomes.
Retry 429 and 500 with backoff
If attempts remain, wait for a delay that doubles each retry (exponential backoff), then loop again. This gives a struggling service room to recover instead of hammering it immediately.
Give up cleanly when the budget runs out
Once maxRetries is exceeded, return a clear "try again later" message rather than looping forever or throwing an unhandled error.
Run it against three scenarios — rate-limited-then-success, an auth failure, and persistent server errors — and the exact same function produces three different, correct outcomes.
Try the working example
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Builds a deterministic mock API call: each call returns the next
// status code in a fixed, pre-scripted sequence.
function createMockApiCall(statusSequence) {
let index = 0;
return function mockApiCall() {
const status = statusSequence[Math.min(index, statusSequence.length - 1)];
index++;
return { status };
};
}
async function callWithHandling(mockApiCall, options = {}) {
const maxRetries = options.maxRetries ?? 3;
const initialDelayMs = options.initialDelayMs ?? 50;
const log = [];
let delay = initialDelayMs;
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
const response = mockApiCall();
log.push(`Attempt ${attempt}: received ${response.status}`);
if (response.status === 200) {
log.push("Result: success.");
return { ok: true, message: "Loaded successfully.", attempts: attempt, log };
}
if (response.status === 401) {
log.push("Result: stopped (not retryable).");
return { ok: false, message: "Please sign in again.", attempts: attempt, log };
}
if (response.status === 403) {
log.push("Result: stopped (not retryable).");
return { ok: false, message: "You don't have access to this.", attempts: attempt, log };
}
if (response.status === 404) {
log.push("Result: stopped (not retryable).");
return { ok: false, message: "That item couldn't be found.", attempts: attempt, log };
}
if (response.status === 429 || response.status === 500) {
const label = response.status === 429 ? "rate limited" : "server error";
if (attempt > maxRetries) {
log.push("Result: gave up after max retries.");
const msg =
response.status === 429
? "Still busy after several tries. Please try again shortly."
: "The service is having trouble. Please try again shortly.";
return { ok: false, message: msg, attempts: attempt, log };
}
log.push(`Result: ${label}, backing off ${delay}ms then retrying.`);
await sleep(delay);
delay *= 2; // exponential backoff
continue;
}
log.push("Result: stopped (unrecognized status).");
return { ok: false, message: "Something unexpected happened.", attempts: attempt, log };
}
}
async function main() {
console.log("--- Scenario A: rate limited twice, then succeeds ---");
const callA = createMockApiCall([429, 429, 200]);
const resultA = await callWithHandling(callA, { maxRetries: 3, initialDelayMs: 20 });
console.log(resultA.log.join("\n"));
console.log(`Final: ok=${resultA.ok} message="${resultA.message}" attempts=${resultA.attempts}`);
console.log("\n--- Scenario B: needs auth ---");
const callB = createMockApiCall([401]);
const resultB = await callWithHandling(callB, { maxRetries: 3, initialDelayMs: 20 });
console.log(resultB.log.join("\n"));
console.log(`Final: ok=${resultB.ok} message="${resultB.message}" attempts=${resultB.attempts}`);
console.log("\n--- Scenario C: server errors exceed retry budget ---");
const callC = createMockApiCall([500, 500, 500, 500]);
const resultC = await callWithHandling(callC, { maxRetries: 2, initialDelayMs: 20 });
console.log(resultC.log.join("\n"));
console.log(`Final: ok=${resultC.ok} message="${resultC.message}" attempts=${resultC.attempts}`);
}
main();--- Scenario A: rate limited twice, then succeeds ---
Attempt 1: received 429
Result: rate limited, backing off 20ms then retrying.
Attempt 2: received 429
Result: rate limited, backing off 40ms then retrying.
Attempt 3: received 200
Result: success.
Final: ok=true message="Loaded successfully." attempts=3
--- Scenario B: needs auth ---
Attempt 1: received 401
Result: stopped (not retryable).
Final: ok=false message="Please sign in again." attempts=1
--- Scenario C: server errors exceed retry budget ---
Attempt 1: received 500
Result: server error, backing off 20ms then retrying.
Attempt 2: received 500
Result: server error, backing off 40ms then retrying.
Attempt 3: received 500
Result: gave up after max retries.
Final: ok=false message="The service is having trouble. Please try again shortly." attempts=35-minute try-it
Extend callWithHandling so it also logs a clear, distinct message for a 403 ("forbidden") separate from a 401 ("needs auth") — the two are often confused but call for different user-facing guidance. Then create a fourth scenario using createMockApiCall([500, 429, 500, 200]) with maxRetries: 3, run it, and predict the attempt count and final message before you check the actual output.
One important caution
Retrying every failure the same way, including 401/403/404 — this wastes time and rate-limit quota on errors that retrying can never fix.
Using a fixed retry delay instead of backoff — hammering a struggling service at a constant interval makes recovery harder, not easier.
MDN — Using the Fetch API (error handling patterns) — API Integration & Webhooks
API Integration Glossary — Common Terms
| Term | Meaning |
|---|---|
| Timeout | A limit on how long a client waits for a response before giving up on the request. |
| Retry | Sending the same request again after a failure, usually only for failures likely to be temporary. |
| Backoff | Waiting progressively longer between each retry attempt instead of retrying immediately. |
| Jitter | A small random amount added to a backoff delay so many clients retrying at once don't all collide at the exact same moment. |
| Idempotency | The property of an operation producing the same end result no matter how many times it's safely repeated. |
| Idempotency Key | A unique value sent with a request so the server can recognize and safely ignore an accidental duplicate. |
| Secret Management | The practice of storing and handling API keys, tokens, and passwords so they never end up exposed in code or logs. |
| Environment Variable | A value set outside the codebase (like a secret or a config setting) that the running program reads at startup. |
| Response Validation | Checking that a response actually has the fields and shape your code expects before using it. |
| SDK | A software development kit — a pre-built library from a provider that wraps their raw API in easier, ready-made functions. |
| Webhook | A URL you register with a provider so it can push you an HTTP request the moment an event happens, instead of you polling for it. |
| Webhook Payload | The JSON body a webhook request carries, describing what event occurred and its details. |
| Webhook Signature | A cryptographic value included in a webhook request that lets you verify it genuinely came from the provider. |
| HMAC | Hash-based Message Authentication Code — the algorithm commonly used with a shared secret to generate and check webhook signatures. |
| Replay Attack | Resending a previously valid, captured request to trick a system into processing it again as if it were new. |
| Webhook Retry | A provider's own automatic re-sending of a webhook if your endpoint didn't respond successfully the first time. |
| Polling | Repeatedly asking an API "has anything changed?" on a schedule, as an alternative to receiving webhooks. |
| Rate Limit Headers | Response headers (like X-RateLimit-Remaining) that tell you how much of your request quota is left before you hit a 429. |
| Postman Environment | A named set of variables (like base URL or token) in Postman that you can switch between, e.g. dev vs. production. |
| Postman Collection | A saved, organized set of requests in Postman that can be shared, reused, or run together as a suite. |
| Test Case | A specific scenario with an expected outcome, used to check that an integration behaves correctly under that condition. |
| Automated API Testing | Running test cases against an API automatically (e.g. via a Postman collection or a script) instead of checking manually every time. |
| API Documentation | A provider's written reference describing how to call their API — endpoints, auth, parameters, responses, and errors. |
| API Documentation Checklist | The fixed set of facts (base URL, auth, endpoint, params, headers, body, response, errors, rate limit) to locate in any new API's docs. |
| Third-Party API | An API owned and operated by a different company than the one integrating with it. |
| Backend Proxy | Your own server sitting between the frontend and a third-party API, so secrets stay server-side and never reach the browser. |
| Credential | Any secret value (API key, token, password) used to prove identity or authorization to a system. |
| Cache | A temporary local copy of a response kept so repeated requests for the same data don't need to hit the API again. |
| Monitoring | Ongoing tracking of an integration's health (error rates, latency, failures) so problems are caught before users report them. |
| API Version | A label (like v1, v2) identifying which revision of an API's behavior and shape you're calling. |
| Deprecation | A formal notice that an API version or feature will stop working at a future date, so integrations need to migrate ahead of it. |
| Bearer Token (refresher) | A credential sent in the Authorization header as "Bearer <token>" — whoever holds it is treated as authenticated. |
| API Key (refresher) | A simple, static credential identifying and authorizing the calling application, usually sent as a header or query parameter. |
| Authentication vs Authorization (refresher) | Authentication proves who is calling; authorization decides what that caller is allowed to do. |
API Integration & Troubleshooting Checklist
| Situation | What to do |
|---|---|
| A request keeps timing out | Confirm a sane timeout is set (don't wait forever), then apply retry-with-backoff rather than retrying instantly in a tight loop. |
| You got a 429 Too Many Requests | Back off and retry with an increasing delay, honor any Retry-After or rate-limit headers the response provides, and cap the retry count. |
| A request seems to have succeeded twice | Use an idempotency key on write operations so a genuine duplicate (from a retry or double click) is safely ignored by the server instead of applied twice. |
| A webhook endpoint is receiving requests you're not sure are genuine | Verify the webhook signature (HMAC) against your shared secret before trusting or processing the payload at all. |
| The same webhook event arrived twice | Treat this as expected provider behavior, not a bug — store processed event IDs and skip any you've already handled. |
| A third-party response is missing an expected field | Add response validation before using the data, and fail with a clear internal error instead of letting undefined silently flow through your app. |
| You're not sure whether to use the SDK or raw API | Prefer the official SDK when one exists for your language — it already handles auth headers, retries, and pagination correctly; fall back to raw calls only for gaps it doesn't cover. |
| You need to test more than the happy path | Write test cases for each realistic status (401, 403, 404, 429, 500) using a mock, not just the 200 case — automate them so they run on every change. |
| You're integrating an unfamiliar API for the first time | Run the documentation checklist first (base URL, auth, endpoint, params, headers, body, response, errors, rate limit) before writing any integration code. |
| A secret credential might have leaked | Rotate (revoke and reissue) the credential immediately through the provider's dashboard — don't just remove it from code, since the old value is already compromised. |
| Deciding whether an API call should happen from the frontend or backend | If the call needs a secret credential, route it through a backend proxy — never embed a secret key in frontend code, where anyone can read it. |
| An integration that worked yesterday broke today | Check the provider's changelog for a version bump or deprecation notice before assuming your own code is at fault. |