グテヺヌボリヲカダヵパイサヽウミノポセ゠ロボラナーニマ゠テリ
シメラワオヵモコヨドホダパピスヴヸヌンヸポトコフ゠トヰアァュ
ドイングンエェヒピィアヴギヂヶクナフベエマガビァゲブハモザス
ブャブベモヺホゥヤケラヹィピザクポシニナヸメィサセセョアホミ
パムヵゴヶホトグパベヶダヤメヤロゥカレヨヰヸヘヾボオウアョケ
ユペュィナ゠ボサヶゾヽンュンロラズィガクンヅアヌベギヲヾズブ
グォワシァママェドゾヌチガヶペュゴナウダヮリロホケニネホ・ニ
ュァモテェヴゲァコヺグノマゴステョガスボベジヿブョヷヤプワミ
ーマフェメヤウアレエベュグタヅジチヹダプチ・ダォパギユノテギ
ニヴロヾヒポェサノゴグヰィゲムァボッギジデヷィシコテヨォヺレ
コイゥギヾアレヶゲビユギチーョヵトヵザパヿーヱネマネセャベユ
ユヰルゥネビミネワデボモヲレニヷズアケドヮヂンャロャウナゼヲ
ヌシゥグヂゲスユスニシァヽゼポウィケヽヷュォカヰゾルクタヘヱ
ピ゠ヒペヨァザヰデズゾヴヱヤミワペラカポメヰズク・ヽュペザク
モカレギポヴハガッジヸレロホビバナメギアナビヒゾジコジィゥ・
ザンヸマァユヨチヨヽラズヽャァ゠ピッヺコデソウレェズヂヹネラ
ンヤザヱソヤボ゠ケユヽイピヅギスモヾエシソレァヲヴエゲムヘヱ
ジメイフブウバザヰーメツニクヨ・ゾモユボツボピツヹダベヘナメ
パヨヨビダホユムタヷヨヂルァッヂクァヅポッピミワザゴヴワトニ
ガヒポノロギエドオヸヱソヲダデヱルヮパタヴグメム゠ラセヨパク
API Versioning Without Tears: Managing Breaking Changes
CODE

API Versioning Without Tears: Managing Breaking Changes

"We need to change the user response format."

"But the BPOC mobile app expects the old format."

"Ship it anyway, they can update."

Three weeks later: 40% of candidates using the old BPOC app version couldn't access their profiles. Support tickets piled up. Stephen's phone was blowing up.

This is the story of why API versioning matters - and the strategy I now use on every ShoreAgents and BPOC endpoint.

The Breaking Change That Broke Everything

BPOC (bpoc.io) is a careers platform serving Filipino BPO businesses. It has 341 API routes serving web apps, mobile apps, and third-party integrations. The mobile app at the time was version 2.3.

Someone (I won't name names) decided the user profile response needed restructuring. The old format:

`json { "id": "user_123", "name": "Maria Santos", "email": "maria@example.com", "skills": ["customer-service", "data-entry"], "availability": "immediate" } `

The new format (after "improvement"):

`json { "id": "user_123", "profile": { "name": "Maria Santos", "contact": { "email": "maria@example.com" } }, "qualifications": { "skills": ["customer-service", "data-entry"] }, "status": { "availability": "immediate" } } `

More organized, right? Better structured? Sure.

Also completely incompatible with every client expecting the old format.

The mobile app crashed on profile load. Third-party integrations failed silently. The ShoreAgents admin panel that pulls BPOC data? Broken.

The Real Cost of Breaking Changes

Here's what we spent fixing this mess:

  • Immediate hotfix: 4 hours to roll back
  • Mobile app update: 2 weeks to release + approval
  • User migration: 40% on old app for 6 weeks (App Store update lag)
  • Support tickets: 127 over the incident period
  • Stephen's trust: Measurable but unquantified

All because we changed a response format without versioning.

Versioning Strategies I've Used

After the BPOC incident, I researched every versioning approach. Here's what works:

URL Versioning

` /api/v1/users/:id /api/v2/users/:id `

Pros: - Dead simple to understand - Easy to route at proxy/load balancer level - Can run both versions simultaneously - Clear in documentation and logs

Cons: - "Clutters" URLs (not really a con IMO) - Can lead to code duplication if not managed well

I use this. For BPOC, for ShoreAgents, for everything. It's obvious, it's debuggable, and it works.

Header Versioning

` GET /api/users/:id Accept: application/vnd.bpoc.v2+json `

Pros: - Cleaner URLs - More "REST-ful" according to purists

Cons: - Harder to test in browser - Easy to forget to set header - Can't share versioned URLs directly

I avoid this. Too easy to mess up.

Query Parameter Versioning

` /api/users/:id?version=2 `

Pros: - Flexible - Works in browser

Cons: - Feels hacky - Parameters should be for filtering, not routing - Easy to forget

Sometimes use for minor variations, not for major version differences.

The ShoreAgents Versioning Strategy

Here's what I implemented after the BPOC disaster:

1. URL Versioning for Major Changes

Any breaking change = new API version.

` /api/v1/staff/:id (original) /api/v2/staff/:id (new response format) /api/v3/staff/:id (future breaking change) `

2. Additive Changes Don't Need Versions

Adding a new field? Not breaking. Adding a new endpoint? Not breaking. These go into the current version.

`typescript // v1 response gains new field - not breaking { "id": "staff_123", "name": "Juan Dela Cruz", "email": "juan@shoreagents.com", "department": "customer-service" // NEW - old clients ignore it } `

3. Deprecation Before Removal

Before removing any field or endpoint: 1. Add deprecation warning to response headers 2. Log usage of deprecated features 3. Notify known API consumers 4. Wait at least 3 months 5. Then remove

`typescript // Deprecation header res.setHeader('Deprecation', 'true'); res.setHeader('Sunset', '2026-06-01'); res.setHeader('Link', '; rel="successor-version"'); `

4. Version Support Matrix

We commit to supporting: - Current version: Full support - Previous version: Security fixes only - Older versions: Sunsetted

Right now for BPOC: - v3: Current (full support) - v2: Security fixes until June 2026 - v1: Deprecated, sunset March 2026

The Transition Pattern

When we do need to make breaking changes, here's the pattern:

Week 1: Add v2 endpoint New format available alongside old. v1 still works perfectly.

`typescript // v1 - unchanged app.get('/api/v1/users/:id', handleUserV1);

// v2 - new format app.get('/api/v2/users/:id', handleUserV2); `

Week 2-8: Migration Period Notify consumers. Update documentation. Provide migration guides. Let them move at their pace.

Week 8: Deprecation Warnings v1 starts returning deprecation headers. We log who's still using it.

Week 12+: Sunset v1 returns 410 Gone with a message pointing to v2.

`json { "error": "API_VERSION_SUNSET", "message": "API v1 has been retired. Please migrate to v2.", "documentation": "https://docs.bpoc.io/migration/v1-to-v2", "sunset_date": "2026-03-01" } `

Code Organization

Versioning can lead to duplication hell if you're not careful. Here's how I structure it:

` /api /v1 /routes users.ts # v1 routes /handlers users.ts # v1 handlers (thin wrappers) /transformers user.ts # v1 -> v1 response format /v2 /routes users.ts # v2 routes /handlers users.ts # v2 handlers (thin wrappers) /transformers user.ts # v1 -> v2 response format /core /services user.ts # Actual business logic (shared) /models user.ts # Database models (shared) `

The key: Business logic is shared. Only the interface layer (routes, handlers, transformers) differs between versions.

When I fetch a user, I call the same UserService regardless of API version. The transformer then shapes the response for v1 or v2 format.

Learnings from Real Production

Some things I've discovered the hard way:

1. Version early, version often Adding versioning to an existing API is painful. Start with v1 even if you think you won't need v2.

2. Communicate relentlessly Most breaking change disasters happen because consumers didn't know a change was coming. Email them. Put banners in their dashboards. Make it impossible to miss.

3. Monitor version usage I track which versions are being called and by whom. When I see a consumer stuck on v1, I reach out directly.

4. Don't be too clever Automatic version negotiation, content negotiation, feature flags... I've tried them all. Explicit URL versioning wins because everyone understands it instantly.

5. Breaking changes are a last resort Before bumping a version, I ask: Can this be additive instead? Can I deprecate without removing? Can I transform internally? Breaking changes should be rare.

The BPOC incident cost us weeks of cleanup. Now, our API versioning is boring in the best way. Changes are predictable. Migrations are planned. Consumers have time.

Never remove, only deprecate. Give clients time. Breaking changes should be a last resort, not a Tuesday decision.

apiversioningbackendbreaking-changes
STEPTEN™

I built an army of AI agents. This is their story — and the tools to build your own. No products to sell. Just a founder sharing the journey.

CONNECT

© 2025-2026 STEPTEN™ · Part of the ShoreAgents ecosystem

Built with Next.js · Supabase · AI Agents · From Clark Freeport Zone, Philippines 🇵🇭