DEV Community

Cover image for API Versioning Tests: 3 Cases Almost Every Team Misses
Sushant Joshi
Sushant Joshi

Posted on • Edited on

API Versioning Tests: 3 Cases Almost Every Team Misses

Our v2 API broke v1 clients in production on a Wednesday because nobody had tested the case where a v1 client received a v2-shaped response after a load-balancer routing rule was tweaked.*

The outage lasted less than an hour.

The damage lasted much longer.

A mobile application started failing silently because it expected a field named:

{
  "customerName": "John Smith"
}
Enter fullscreen mode Exit fullscreen mode

but suddenly received:

{
  "fullName": "John Smith",
  "customerStatus": "Active"
}
Enter fullscreen mode Exit fullscreen mode

Nothing was technically wrong with the v2 API.

The issue was that some v1 clients unexpectedly started receiving v2 responses.

The routing change looked harmless during deployment.

No tests covered that scenario.

This experience taught us an important lesson:

Versioning an API isn't just about creating /v2.

It's about ensuring every version continues behaving exactly as its consumers expect.

Most teams write happy-path tests for each version independently. Very few teams write tests that validate interactions between versions, backward compatibility guarantees, and accidental cross-version behavior.

These are the three categories of API versioning tests that almost every team misses.


Why Versioning Tests Matter More Than Ever

Modern APIs evolve constantly.

New versions introduce:

  • Additional fields
  • Renamed properties
  • New authentication requirements
  • Performance improvements
  • Business logic changes

None of these are inherently bad.

The risk comes from assuming:

"Since v2 works, v1 must still work too."

That assumption causes many production incidents.

Consumers don't upgrade immediately.

Some clients may continue using older versions for months or years.

A single breaking change can impact:

  • Mobile applications
  • Partner integrations
  • Third-party SDKs
  • Internal microservices
  • Reporting systems

This is why API version testing requires more than endpoint-level validation.

It requires contract validation across versions.


1. The Dual-Version Contract Test

This is the most valuable test we added after our routing incident.

The concept is simple.

Every request that exists in both versions should be tested against both versions simultaneously.

For example:

GET /customers/123
Enter fullscreen mode Exit fullscreen mode

is executed against:

/v1/customers/123
/v2/customers/123
Enter fullscreen mode Exit fullscreen mode

The goal isn't necessarily to make responses identical.

The goal is to validate contractual guarantees.


What the Test Checks

The test should answer:

  • Does v1 still return every promised field?
  • Are field types unchanged?
  • Are required properties still present?
  • Are deprecated properties still available?
  • Are status codes consistent?

This catches situations where implementation changes accidentally impact previous versions.


The Cross-Version Routing Test

Now comes the scenario most teams never test.

What happens if:

v1 request → v2 service
Enter fullscreen mode Exit fullscreen mode

or

v2 request → v1 service
Enter fullscreen mode Exit fullscreen mode

This sounds unlikely.

In distributed systems, it happens more often than people expect.

Examples include:

  • Load-balancer misconfigurations
  • Canary deployments
  • Service discovery issues
  • Proxy rules
  • API gateway changes

Your system should either:

  1. Continue functioning safely.
  2. Return a clear version error.

It should never fail unpredictably.


Example Test Matrix

Request Expected Response
v1 → v1 Success
v2 → v2 Success
v1 → v2 Graceful handling
v2 → v1 Graceful handling

Most teams only test the first two rows.

The last two are where incidents happen.


2. Deprecated-Field Assertions (Yes, You Should Assert They Exist)

This recommendation often surprises people.

Many engineers believe deprecated fields should gradually disappear from tests.

In reality, the opposite is usually true.

If a field is marked as deprecated but remains part of the contract, your tests should explicitly assert its existence.


A Real Example

Version 1 returns:

{
  "customerName": "John Smith"
}
Enter fullscreen mode Exit fullscreen mode

Version 2 introduces:

{
  "fullName": "John Smith"
}
Enter fullscreen mode Exit fullscreen mode

The team marks:

customerName
Enter fullscreen mode Exit fullscreen mode

as deprecated.

Everything seems fine.

Six months later, an engineer removes:

customerName
Enter fullscreen mode Exit fullscreen mode

assuming nobody uses it anymore.

Unfortunately:

  • Mobile apps still depend on it.
  • A partner integration still parses it.
  • Several reports still consume it.

Production breaks.


The Better Approach

Your test should explicitly verify:

expect(response.customerName)
  .toBeDefined();
Enter fullscreen mode Exit fullscreen mode

Even though the field is deprecated.

Deprecation means:

"Planned for removal."

It does not mean:

"Safe to remove today."


Add Expiration Dates

The best practice is to combine assertions with metadata.

For example:

deprecated:
  field: customerName
  remove_after: 2027-01-01
Enter fullscreen mode Exit fullscreen mode

Now tests can remind teams:

  • Field still exists.
  • Deprecation window is active.
  • Removal date is approaching.

This creates a controlled migration process.


3. The Header-Based vs URL-Based Version Test Pattern

API versioning strategies vary significantly.

The two most common approaches are:

URL Versioning

/api/v1/orders
/api/v2/orders
Enter fullscreen mode Exit fullscreen mode

Header Versioning

Accept: application/vnd.company.v1+json
Accept: application/vnd.company.v2+json
Enter fullscreen mode Exit fullscreen mode

Both approaches require different testing strategies.


URL-Based Testing

This approach is relatively straightforward.

Test suites can easily target:

  • /v1
  • /v2
  • /v3

Contract separation is clear.

However, routing issues become important.

You need tests that verify:

  • URL rewrites
  • Gateway behavior
  • Redirect handling

Header-Based Testing

Header-based versioning creates additional complexity.

The same endpoint:

GET /orders
Enter fullscreen mode Exit fullscreen mode

can return completely different responses depending on headers.

Testing must validate:

  • Missing version headers
  • Invalid versions
  • Default versions
  • Unsupported versions
  • Version negotiation behavior

A Test Pattern We Like

For every endpoint:

No header
v1 header
v2 header
invalid version header
future version header
Enter fullscreen mode Exit fullscreen mode

This often reveals unexpected behavior.

Examples include:

  • Silent fallbacks
  • Incorrect defaults
  • Cached responses
  • Improper content negotiation

Snapshot Diffs Between v1 and v2 Responses

One of the simplest and most effective versioning techniques is snapshot comparison.

For a representative request:

GET /customers/123
Enter fullscreen mode Exit fullscreen mode

Capture:

v1_response.json
v2_response.json
Enter fullscreen mode Exit fullscreen mode

Then generate structural diffs.


Example Diff

{
- "customerName": "John Smith",
+ "fullName": "John Smith",

+ "customerStatus": "Active"
}
Enter fullscreen mode Exit fullscreen mode

This provides immediate visibility into:

  • Added fields
  • Removed fields
  • Type changes
  • Structural changes

Why Snapshots Matter

Humans often miss small changes during code review.

Examples:

- "123"
+ 123
Enter fullscreen mode Exit fullscreen mode

This appears minor.

For some consumers, it's a breaking change.

Snapshot testing catches these differences immediately.


Contract Snapshots Are Better Than Payload Snapshots

Avoid comparing exact values whenever possible.

Compare:

  • Field names
  • Types
  • Required properties
  • Structure

This reduces noise while preserving meaningful change detection.


The CI Gate That Catches Accidental Breaking Changes

The most effective versioning tests don't run only during releases.

They run continuously.

Every pull request should answer one question:

Does this change accidentally break a previous contract?


What the CI Gate Should Check

Schema Changes

  • Required fields removed
  • Data types changed
  • Enum values changed

Response Changes

  • Deprecated fields removed
  • Status codes changed
  • Error structures modified

Version Behavior

  • Routing changes
  • Header handling
  • Content negotiation

Example CI Pipeline

Build
↓
Run version contract tests
↓
Generate snapshots
↓
Compare against baseline
↓
Fail if breaking changes detected
Enter fullscreen mode Exit fullscreen mode

The process is simple.

The impact is enormous.


The Breaking Changes You Want to Catch

Examples include:

- customerId: integer
+ customerId: string
Enter fullscreen mode Exit fullscreen mode
- status: Active
+ state: Active
Enter fullscreen mode Exit fullscreen mode
- customerName
Enter fullscreen mode Exit fullscreen mode

These changes may look harmless to developers.

For API consumers, they can be catastrophic.


Building a Sustainable Version Testing Strategy

A practical strategy usually includes four layers.


Layer 1: Endpoint Tests

Validate each version independently.


Layer 2: Contract Tests

Validate compatibility between versions.


Layer 3: Snapshot Diffs

Highlight structural changes.


Layer 4: CI Gates

Prevent accidental regressions from reaching production.


This layered approach dramatically reduces version-related incidents.

It also provides confidence when introducing new versions.


Why Teams Still Miss These Tests

Versioning issues often occur because teams think in terms of endpoints.

Consumers think in terms of contracts.

Those perspectives are different.

An endpoint can function perfectly while still breaking consumers.

The goal of version testing is not simply:

"Does the endpoint return 200?"

The goal is:

"Can every supported consumer continue working safely?"

That question requires broader testing.


Final Thoughts

The most painful API incidents I've seen were not caused by servers crashing or databases failing.

They were caused by small compatibility assumptions.

A renamed field.

A removed property.

A routing rule.

A header negotiation bug.

These changes rarely trigger alarms immediately.

Instead, they quietly break integrations that nobody remembered to test.

Strong backward compatibility tests and thoughtful deprecation testing dramatically reduce those risks.

Versioning isn't just about introducing new APIs.

It's about preserving trust with every client that still depends on older ones.

If your team is building or maintaining multiple API versions, adopting the contract-testing approach we use for versioned APIs can provide a much safer foundation for change:

Because in versioned systems, the bugs that hurt the most are usually the ones nobody thought to test.

Top comments (0)