Why this matters
Backend engineers evolve APIs constantly: adding fields, fixing naming, improving pagination, or launching new flows. If you break existing clients (mobile apps, partner integrations), you trigger outages, rollbacks, and support tickets. Versioning and backward compatibility help you ship improvements safely while users continue working without surprises.
- Real tasks: Add a new attribute for analytics without breaking older app versions.
- Real tasks: Sunset a deprecated endpoint with a clear migration path and timeline.
- Real tasks: Introduce a v2 with cleaner contracts while keeping v1 stable until usage drops.
Note: Everyone can take the Quick Test; only logged-in users have saved progress.
Concept explained simply
Your API is a contract. Backward-compatible changes keep old clients happy while you improve the contract. Breaking changes require coordination and versioning so clients can opt in when ready.
Mental model
Think of versioning like traffic lanes on a highway:
- Lane v1: Existing cars keep cruising safely.
- Lane v2: A newer, smoother lane. Cars change lanes when ready.
- Speed signs (deprecation notices) tell drivers when a lane will close.
Versioning strategies
1) URI versioning (e.g., /v1/users)
Simple and explicit. Easy caching, logging, and routing. Clear separation but duplicates routes and docs.
2) Header-based versioning (e.g., Accept: application/vnd.api+json;version=2)
Keeps URLs stable and lets clients negotiate versions. Slightly harder to debug via browser but great for avoiding URL churn.
3) Resource-level evolution (no global versions)
Prefer compatible changes in-place (additive). Use deprecation signals and remove only after a safe window.
4) Message schema versioning
Use explicit schema versions in payloads or Protobuf/Avro evolution rules. Reserve fields and avoid reusing tags/ids.
Backward compatibility rules
Generally safe (compatible)
- Adding optional response fields.
- Adding new endpoints.
- Adding optional request parameters with sane defaults.
- Reordering JSON object fields.
- Extending enum with new values if clients ignore unknowns or handle default cases.
Breaking (incompatible)
- Renaming or removing fields used by clients.
- Changing a field type (e.g., number to string).
- Making a previously optional request field required.
- Altering semantics (e.g., changing currency or time unit without versioning).
Responses: design for forward-compatibility
- Clients should ignore unknown fields.
- Use additive patterns: instead of rename, add new field and deprecate the old.
- Use new endpoints for significantly different behavior.
Deprecation and lifecycle
- Announce: add a deprecation header or response field with a date and migration notes.
- Monitor: track usage and identify top consumers.
- Support window: keep old behavior for a defined period (e.g., months) to allow upgrades.
- Sunset: after the window, enforce changes (e.g., 410 Gone) with clear error messages.
Worked examples
Example 1: Add an optional field
Current v1 GET /users/{id} returns { id, name }.
Goal: add is_premium without breaking v1 clients.
- Change: include is_premium as an optional boolean in response.
- Result: old clients continue using id and name; new clients can use is_premium.
Response before vs after
{
"id": "u_123",
"name": "Sam"
}
{
"id": "u_123",
"name": "Sam",
"is_premium": false
}Example 2: Rename a field safely
Current: field phone (string). You want phones (array).
- Phase 1 (v1 additive): add phones; keep phone; document phones as preferred.
- Phase 2 (deprecate): mark phone as deprecated; notify clients.
- Phase 3 (v2): remove phone; only phones remains.
Transitional response
{
"id": "u_123",
"phone": "+12025550123", // deprecated
"phones": ["+12025550123"]
}Example 3: Enum evolution
Current: status ∈ {"pending","active"}. You need "paused".
- If clients treat unknown as a safe default or display-only, you can add "paused" in-place.
- If clients switch on exact enum values, add via versioning or communicate expectations clearly.
How to plan a change (step-by-step)
- Identify impact: fields touched, clients affected, metrics to watch.
- Classify: compatible or breaking?
- Choose strategy: in-place addition, dual fields, or new version/endpoint.
- Design rollout: deprecation headers/messages, dates, and monitoring plan.
- Implement guards: feature flags, canary, and logs for fallback.
- Document: changelog, migration notes, examples.
- Sunset: remove after the support window passes.
Testing for compatibility
- Schema checks: validate examples against a schema; forbid breaking changes in CI.
- Contract tests: ensure consumers still work with new responses (consumer-driven contracts if available).
- Canary: release to a subset; verify error rates and logs.
- Playback tests: replay real requests to new version and compare responses.
Exercises
Complete these tasks, then compare your answers with the solutions. Tip: write before peeking.
Exercise 1 – Add a field without breaking clients (ex1)
Scenario: v1 GET /users returns { id, name, email }. Product wants middle_name for richer profiles. Update the API to include middle_name without breaking existing clients. Provide:
- Updated example response.
- Notes on defaults/absence.
- Any docs or deprecation text needed.
Hints
- Prefer additive responses.
- Middle name might be null or omitted. Decide which and document.
Exercise 2 – Plan a breaking rename (ex2)
Scenario: v1 GET /users returns phone (string). You need phones (array of strings) and will eventually remove phone. Design a rollout plan covering:
- Phases and timelines.
- How to signal deprecation.
- Monitoring and final removal criteria.
Hints
- Dual-write/dual-read for a period.
- Use deprecation headers or fields.
- Define a sunset date with usage targets.
Self-check checklist
- Did you keep existing clients working without code changes in the short term?
- Is the migration path documented and testable?
- Do you have a monitoring plan to know when to remove deprecated fields?
Common mistakes and how to self-check
- Mistake: Renaming fields in-place. Fix: Add new field, deprecate old, remove in v2.
- Mistake: Making optional parameters required. Fix: Introduce new behavior behind a new version or parameter with default.
- Mistake: Silent behavior changes. Fix: Version and document; add deprecation headers and clear error messages.
- Mistake: No sunset plan. Fix: Set dates, thresholds, and communicate early.
Practical projects
- Build a tiny contacts API with v1 and a planned v2. Demonstrate a non-breaking addition and a breaking rename via versioning.
- Create an API changelog and deprecation policy page for your demo API.
- Add CI checks to block removing fields referenced in sample consumer tests.
Learning path
- Start: HTTP and JSON basics.
- Then: API design principles (nouns, resources, pagination).
- This lesson: Versioning, compatibility, and deprecation.
- Next: Authentication changes and permission evolution without breakage.
Who this is for
- Backend engineers maintaining public or internal APIs.
- Full-stack engineers evolving backend endpoints used by web/mobile clients.
- Platform engineers designing service-to-service contracts.
Prerequisites
- Comfortable with HTTP methods, status codes, and headers.
- JSON or similar data formats.
- Basic CI usage for validation and tests.
Mini challenge
You need to switch pagination from page/limit to cursor-based. Propose a rollout plan that keeps old clients functioning, lets new clients adopt cursor-based pagination, and includes a sunset strategy for page/limit.
Next steps
- Draft a deprecation policy template for your team.
- Identify one endpoint you can improve safely this week.
- Implement schema checks and simple contract tests in CI.