ScaledByDesign/Insights
ServicesPricingAboutContact
Book a Call
Scaled By Design

Fractional CTO + execution partner for revenue-critical systems.

Company

  • About
  • Services
  • Contact

Resources

  • Insights
  • Pricing
  • FAQ

Legal

  • Privacy Policy
  • Terms of Service

© 2026 ScaledByDesign. All rights reserved.

contact@scaledbydesign.com

On This Page

The Version 7 ProblemThe Three ApproachesApproach 1: URL VersioningApproach 2: Header VersioningApproach 3: Evolution Without Versioning (Our Recommendation)The Additive-Only StrategyWhen You Actually Need a New VersionThe Deprecation ProcessThe API Contract Testing Pattern
  1. Insights
  2. Architecture
  3. API Versioning Strategies That Don't Become a Maintenance Nightmare

API Versioning Strategies That Don't Become a Maintenance Nightmare

March 30, 2026·ScaledByDesign·
apiversioningrestarchitecturebackend

The Version 7 Problem

A client had 7 active API versions. Each version had subtle differences in response shapes, validation rules, and business logic. Their backend team spent 40% of their time maintaining backward compatibility across versions. New features took 3x longer because every change had to work across v3 through v7. They wanted to deprecate old versions but their largest customer was still on v3.

API versioning is one of those decisions that feels simple when you make it and becomes a permanent tax on your engineering team.

The Three Approaches

Approach 1: URL Versioning

GET /v1/orders
GET /v2/orders
GET /v3/orders

Pros: Explicit, easy to understand, easy to route, easy to log and monitor per-version.

Cons: Encourages creating too many versions. Every breaking change gets a new version. Clients hardcode version in URLs making migration harder.

Best for: Public APIs with many external consumers. The explicitness helps third-party developers.

Approach 2: Header Versioning

GET /orders
Accept: application/vnd.company.v2+json

Pros: Clean URLs, version is metadata not part of the resource path. Easier to default clients to latest.

Cons: Harder to test (can't just paste URL in browser). Easy to forget the header. API gateways need explicit configuration.

Best for: Internal APIs where you control all clients.

Approach 3: Evolution Without Versioning (Our Recommendation)

GET /orders
// Response always includes all fields
// New fields are additive — never remove or rename
// Deprecated fields return null/default but still exist

Pros: No version maintenance, no duplication, one codebase. Forces you to think about backward compatibility with every change.

Cons: Requires discipline. Can't make truly breaking changes. Response payloads grow over time.

Best for: Most APIs. Seriously.

The Additive-Only Strategy

The key insight: most "breaking changes" don't need to break anything. With discipline, you can evolve an API indefinitely without versioning:

// Rule 1: NEVER remove a field
// BAD:  Remove "name" from the response
// GOOD: Add "fullName", keep "name" as an alias
 
// Rule 2: NEVER rename a field
// BAD:  Rename "createdAt" to "created_at"
// GOOD: Add "created_at", keep "createdAt" returning the same value
 
// Rule 3: NEVER change a field's type
// BAD:  Change "price" from number to string
// GOOD: Add "priceFormatted" as string, keep "price" as number
 
// Rule 4: New required fields have defaults
// BAD:  Add required "tier" field to POST /customers
// GOOD: Add optional "tier" field, default to "standard"

Example response evolution over 3 years:

// Year 1 response
{
  "id": "ord_123",
  "name": "Order #123",
  "total": 99.50,
  "created": "2024-01-15T10:00:00Z"
}
 
// Year 3 response (same endpoint, no version change)
{
  "id": "ord_123",
  "name": "Order #123",        // Still here for backward compat
  "displayName": "#ORD-123",   // New, better formatted name
  "total": 99.50,              // Still a number
  "totalFormatted": "$99.50",  // New, pre-formatted string
  "currency": "USD",           // New field
  "created": "2024-01-15T10:00:00Z",  // Still here
  "createdAt": "2024-01-15T10:00:00Z", // New, consistent naming
  "status": "shipped",         // New field
  "metadata": {}               // New field
}

Old clients still work perfectly. They just ignore the new fields. New clients get richer data. No version bump required.

When You Actually Need a New Version

Sometimes a truly breaking change is unavoidable:

Version-worthy changes:
  → Fundamentally changing authentication mechanism
  → Restructuring the entire resource model
  → Changing pagination strategy
  → Legal/compliance requirements that alter data shape

NOT version-worthy (use additive approach):
  → Adding new fields
  → Adding new endpoints
  → Changing validation rules (make them more permissive)
  → Adding new query parameters
  → Deprecating a field (keep it, return default value)

The Deprecation Process

When you do need to retire old behavior:

Month 0:  Announce deprecation (changelog, email, API response header)
          Add header: Deprecation: true; Sunset: 2026-09-01
          
Month 1:  Contact top 20 consumers directly
          Provide migration guide with code examples
          
Month 3:  Add deprecation warnings to logs
          Monitor usage of deprecated endpoints/fields
          
Month 6:  Final warning. Usage should be near zero.
          If any major consumer remains, negotiate timeline.
          
Month 9:  Remove deprecated code (or return 410 Gone)

The biggest mistake is deprecating without monitoring. If you remove a field and 500 clients break, you'll be rolling back at 2 AM. Know who's using what before you remove anything.

The API Contract Testing Pattern

Prevent accidental breaking changes with contract tests:

// api-contracts/orders.test.ts
describe("GET /orders/:id contract", () => {
  it("always returns required fields", async () => {
    const response = await request(app).get("/orders/ord_test_123");
    
    // These fields must ALWAYS exist (backward compatibility)
    expect(response.body).toHaveProperty("id");
    expect(response.body).toHaveProperty("name");
    expect(response.body).toHaveProperty("total");
    expect(response.body).toHaveProperty("created");
    
    // Type assertions (types must never change)
    expect(typeof response.body.id).toBe("string");
    expect(typeof response.body.total).toBe("number");
  });
});

Run these in CI on every PR. If a contract test fails, someone is about to make a breaking change — and the test stops them before it reaches production.

API versioning isn't about picking the right strategy. It's about avoiding the need for versions in the first place. Design for evolution, enforce additive-only changes, and save versioning for the truly breaking changes that happen once every few years.

Previous
Your A/B Test Isn't Statistically Significant — Here's What to Do About It
Insights
API Versioning Strategies That Don't Become a Maintenance NightmareEvent-Driven Architecture Without the PhD — A Practical GuideCQRS Without the Complexity — A Practical Implementation GuideThe Strangler Fig Migration That Saved a 10-Year-Old MonolithWhy You Should Start With a MonolithEvent-Driven Architecture for the Rest of UsThe Real Cost of Microservices at Your ScaleThe Caching Strategy That Cut Our Client's AWS Bill by 60%API Design Mistakes That Will Haunt You for YearsMulti-Tenant Architecture: The Decisions You Can't UndoCI/CD Pipelines That Actually Make You FasterThe Rate Limiting Strategy That Saved Our Client's APIWhen to Rewrite vs Refactor: The Decision Framework

Ready to Ship?

Let's talk about your engineering challenges and how we can help.

Book a Call