NodeJSConsuming Third-Party APIs

Consuming Third-Party APIs

Almost every real Node service is also an API client — it calls Stripe to charge a card, sends mail through SendGrid, asks OpenAI for a completion, or talks to an internal microservice. Calling someone else's API over the network is fundamentally different from calling a local function: it's slow, fallible, and outside your control. The network drops, the remote service is down or rate-limits you, responses come back malformed. Treating remote calls with the respect that unreliability demands — timeouts, retries, error handling, secret management — is what separates a robust integration from one that takes your app down with it. This page covers the lifecycle of an outbound request and the production concerns that wrap it.

The anatomy of an outbound request

JS
// A real third-party call has more concerns than just "fetch the URL":
async function getWeather(city) {
  const res = await fetch(`https://api.weather.com/v1/current?city=${encodeURIComponent(city)}`, {
    method: 'GET',
    headers: {
      Authorization: `Bearer ${process.env.WEATHER_API_KEY}`,  // secret from env, never hard-coded
      Accept: 'application/json',
    },
    signal: AbortSignal.timeout(5000),                          // 1. always set a timeout
  })

  if (!res.ok) {                                                // 2. check the status yourself
    throw new Error(`Weather API ${res.status}: ${await res.text()}`)
  }
  return res.json()                                             // 3. parse the body
}
A remote call = build the request, authenticate, time it out, check the status, parse the body — every step can fail
An outbound API call is a small pipeline, and *each* stage is a failure point. You **build** the request (URL with properly **encoded** query params, method, headers), **authenticate** it (an API key or token pulled from the environment), **bound** it with a timeout so a hung remote can't hang you, **send** it, **check the status** (a `fetch` does *not* throw on `404`/`500` — you must inspect `res.ok` yourself), and finally **parse** the body (and handle the case where it isn't the JSON you expected). Skipping any step is where integrations break in production: no timeout → hung requests pile up; no status check → you parse an error page as data; secret in code → it leaks in git. The next pages cover the two main clients — the built-in [fetch](/nodejs/fetch-api) and [Axios](/nodejs/axios) — but these concerns apply to both.
The network is unreliable — design for failure

Concern

Why it matters

What to do

Timeouts

A remote that never responds hangs your request forever, exhausting connections

Always set one (AbortSignal.timeout); pick a budget (e.g. 3–10s)

Retries

Transient errors (429, 503, network blip) often succeed on a second try

Retry idempotent calls with exponential backoff + jitter

Rate limits

APIs cap your request rate; exceeding it returns 429

Respect Retry-After; throttle your own outbound rate

Circuit breaking

Hammering a down service wastes resources and slows recovery

Trip a breaker after repeated failures; fail fast for a cooldown

Idempotency

A retried POST could double-charge / double-create

Send an idempotency key; only auto-retry safe (idempotent) methods

Never retry a non-idempotent request blindly — a retried payment or order can charge or create twice
Retries are essential, but **what** you retry matters enormously. A `GET` is idempotent — repeating it is harmless — so retrying on a timeout or `503` is safe. A `POST` that charges a card or creates an order is **not** idempotent: if the first request actually succeeded but the *response* was lost (timeout after the server processed it), a blind retry charges the customer twice. Two defenses: only auto-retry methods known to be safe, and for unsafe operations send an **idempotency key** (a unique id the API uses to deduplicate — Stripe, for instance, supports this) so the server recognizes the retry and returns the original result instead of acting again. And always use **exponential backoff with jitter** — retrying immediately and in lockstep across many workers creates a thundering herd that worsens the outage.
Managing secrets and keys

JS
// ❌ NEVER hard-code credentials — they leak into git history forever:
const KEY = 'sk_live_51H8xQ2...'

// ✅ Read from the environment (set via .env locally, secrets manager in prod):
const KEY = process.env.STRIPE_SECRET_KEY
if (!KEY) throw new Error('STRIPE_SECRET_KEY is not set')   // fail fast at startup
API keys live in environment variables / a secrets manager — never in source, never sent to the client
Third-party APIs authenticate you with **secrets** (API keys, OAuth tokens), and how you handle them is a security decision. Keep them in **[environment variables](/nodejs/dotenv)** — a `.env` file (git-ignored) in development, a real secrets manager (AWS Secrets Manager, Vault, your platform's secret store) in production — never hard-coded in source, where they live forever in git history even after deletion. Validate they're present at startup so a missing key fails loudly rather than at the first API call. Critically, **these are server-side secrets**: a third-party *secret* key must never be shipped to the browser or a mobile app, where anyone can read it — proxy those calls through your backend. Rotate keys periodically and scope them to least privilege.
Handle responses defensively
  • Check the status before parsing2xx is success; 4xx is your mistake (bad request, auth, not found); 5xx is theirs (retry candidates).

  • Don't trust the body shape — a third party can change their response or return an error envelope; validate (e.g. with Zod) before using it.

  • Set a timeout on every call — there is no acceptable default of "wait forever".

  • Log failures with context — which API, status, and a correlation id — but redact secrets and PII from what you log.

  • Isolate the blast radius — wrap each integration so one flaky vendor degrades only its feature, not the whole request (timeouts, fallbacks, circuit breakers).

  • Cache when you can — many third-party responses (config, reference data) change rarely; cache them to cut latency, cost, and rate-limit pressure.

A third-party API is untrusted input over an unreliable network — validate the body and contain the failure
Once a response comes back, treat it with the same suspicion as any external input. The **status code** is your first branch: `2xx` succeeded, `4xx` means *you* sent something wrong (fix the request, don't retry), `5xx` means *they* failed (a retry candidate). Beyond the status, the **body itself is untrusted** — vendors change schemas, return error envelopes with a `200`, or send partial data; parse and **validate** it against an expected shape before you rely on it, so a surprise doesn't crash a handler deep in your code. Wrap each integration to **contain failure**: a timeout, a fallback value, or a tripped circuit breaker should let one misbehaving vendor degrade a single feature rather than cascade into a full outage. Robust API consumption is mostly disciplined pessimism.
Next
Start with the client that's now built into Node: [The Built-in fetch API](/nodejs/fetch-api).