API Integration

Handling Inconsistent API Responses: A Developer’s Guide to Staying Sane

Keep vendor quirks at the integration boundary with explicit validation, a stable internal model, and useful failure signals.

3 min read

One endpoint returns an array, another wraps it in data, and a third returns null when no records exist. The difficult part is not writing another conditional. It is preventing those differences from spreading through every component and background job that consumes the integration.

A good adapter absorbs the provider's vocabulary and produces a model the rest of your application can trust. That boundary should be strict about essential facts and deliberate about the variations it accepts.

Separate transport success from useful data

A resolved fetch promise does not necessarily mean a successful HTTP response. Check the status before treating the body as application data. Parsing JSON is another independent operation: an upstream proxy can return an HTML error page, and a successful response can still contain a shape your code does not understand.

Classify these failures separately in logs and internal results. A connection failure, an HTTP rejection, invalid JSON, and a schema mismatch require different investigation. Give callers a stable error category, while retaining a sanitized diagnostic summary and a provider request identifier when available. Avoid placing access tokens or complete customer payloads in routine logs.

Normalize only documented variations

Suppose a provider represents a customer identifier as either a nonempty string or a safe integer. A boundary function can convert those documented forms into an internal string. It should reject an object, an empty value, or an unsafe number rather than turning it into a misleading identifier.

Missing data should retain its meaning. An absent amount is not automatically zero; an unknown delivery date is not today's date. Defaulting everything makes a dashboard look complete while quietly changing the business interpretation. Decide which fields can be omitted, which can be null, and which must stop the operation.

function normalizeId(value: unknown): string {
  if (typeof value === 'string' && value.trim()) {
    return value.trim();
  }
  if (typeof value === 'number' && Number.isSafeInteger(value)) {
    return String(value);
  }
  throw new Error('Provider returned an invalid customer ID');
}

Keep the application independent of provider shapes

Define a small internal representation around the operations the product needs. For a shipment, that might include an internal identifier, a normalized status, an optional expected date, and the provider reference. Map each provider response into that representation once. Components should not need to know which vendor spells a status as in_transit and which uses moving.

Keep an explicit unknown status when the vendor adds a value you cannot interpret safely. A new provider status should trigger investigation, not accidentally mark a shipment delivered. If the product can display partial information, make that state visible to the caller. If it cannot, reject the response at the boundary instead of passing an incomplete object deeper into the application.

Use fixtures to catch drift early

Build fixtures from sanitized, representative responses: empty results, optional fields, a newly introduced enum, a malformed item, and multiple pages. Contract tests can run those fixtures through the adapter without calling the real provider. Keep at least one deliberate failure case so the validation boundary itself is exercised.

Monitor schema failures by provider and endpoint. A sudden increase after a vendor release is easier to investigate when normalization happens in one place. Review accepted variations periodically; a temporary workaround can become an undocumented permanent contract. The goal is a small, understandable translation layer that makes upstream changes visible while keeping normal application code predictable.