Q35 of 37 · API testing

How do you handle backwards-compatible API changes that require client coordination?

API testingSeniorapibackwards-compatibilitydeprecationprocesssenior

Short answer

Short answer: Treat the API change as one of three deploy phases: add new (non-breaking, server-only), migrate clients (rolling, both old and new contracts work), remove old (after telemetry confirms no callers). Tests cover both shapes during the transition. Telemetry on the deprecated endpoint drives the removal date.

Detail

"Backwards-compatible" still requires careful staging. The deploy contract:

Phase 1 — Add new shape, keep old working. Server-side change only. New field added to response, new endpoint added, new field accepted on input. Old clients see no difference; new clients can opt in.

Tests:

  • Existing tests for the old shape still pass (backwards compat).
  • New tests cover the new shape.
  • Both shapes get hit in CI.
test('GET /users returns legacy fields', async () => {
  const res = await request.get('/users/42');
  const body = await res.json();
  expect(body.first_name).toBeDefined();    // legacy
  expect(body.firstName).toBeDefined();     // new
});

Phase 2 — Migrate clients.

  • Update each consumer (web, mobile, partner) to use the new shape.
  • Mark the old fields as deprecated in the schema and add response headers signalling deprecation.
  • Track usage of the old shape via telemetry per caller (X-Caller-Id or auth-derived).

This is where most "backwards compat" plans fail: the old shape lingers for years because nobody chases the last few callers. Tag deprecated fields and make a list of remaining callers visible.

Phase 3 — Remove old shape. Only when telemetry shows zero callers for N weeks. Communicate the change, give a final notice, then remove.

// Test that should fail until phase 3 ships
test('legacy first_name is removed', async () => {
  const res = await request.get('/users/42');
  const body = await res.json();
  expect(body.first_name).toBeUndefined();
});

The cross-team coordination:

  • Engineering: feature flag the new shape; phase the rollout.
  • QA: tests for both shapes during transition; clearly tagged.
  • Customer success / DevRel: communications to public API customers.
  • Product: deadlines for each phase, including a hard "we will remove on date X."

Test strategy specifics:

  • Both-shapes tests during phase 1-2. Unify into one test using expect.objectContaining to cover both:
expect(body).toEqual(expect.objectContaining({
  first_name: expect.any(String),
  firstName: expect.any(String),
}));
  • Removal tests queued (skipped) until phase 3 lands:
test.skip('legacy fields removed (queued for phase 3)', ...);
  • Telemetry assertions: a test that pings the analytics API and asserts "zero callers in the last 30 days" before phase 3 unlocks.

Anti-patterns:

  • Skipping phase 2 — the old shape never gets removed and the API surface area grows.
  • Telling customers "deprecated, removing soon" without a date — nobody acts until the deadline.
  • Tests that quietly cover only the new shape — you lose the regression on backwards compatibility before phase 2 finishes.

The senior signal: thinking in deploy phases, treating telemetry as the gate to phase 3, and explicit cross-team coordination beyond writing tests.

// WHAT INTERVIEWERS LOOK FOR

Three-phase mental model (add, migrate, remove), telemetry-driven removal, both-shapes tests during transition, and cross-team awareness — the work goes beyond writing tests.

// COMMON PITFALL

Adding the new shape and forgetting to remove the old. The 'backwards compatible' becomes 'two shapes forever,' and the surface doubles.