Why this matters
Versioning is how you evolve APIs without breaking apps in production. As an API Engineer, you will: ship new features safely, deprecate legacy fields, coordinate multi-service rollouts, and communicate change timelines. A clear versioning strategy reduces outages, support tickets, and client churn.
- Real tasks you will face: adding fields without breaking mobile clients; removing deprecated endpoints; introducing a new pagination model; evolving protobuf messages; handling GraphQL schema deprecations; setting deprecation and sunset headers; coordinating with SDKs.
Concept explained simply
An API is a contract. Versioning is the rulebook for changing the contract in a predictable way so clients can adapt. Backward-compatible changes flow into the current version; breaking changes require a new version and a migration plan.
Mental model
- Contract: what the client can rely on (shapes, names, behaviors).
- Compatibility budget: every change spends budget. Only non-breaking changes are “free”. Breaking changes require a new major version and runway.
- Migration runway: announce → deprecate → dual-run → sunset.
Versioning options at a glance
- REST path versioning: /v1/users. Simple, cache-friendly, explicit. Best for clear breaking changes.
- REST header/content negotiation: Accept: application/vnd.acme.user+json; version=2. Flexible and can avoid path sprawl; requires clients to set headers.
- REST query param: ?version=2. Easy to try; weaker caching; not ideal for long-term strategy.
- GraphQL: often “versionless” schema with @deprecated on fields, plus additive changes. For rare breaking changes, use a new graph/schema name (e.g., /graphql/v2) or a major API namespace.
- gRPC: use protobuf evolution rules. Prefer staying on the same service name with backward-compatible field changes. For breaking changes, create new package or service name (e.g., user.v2) and reserve old tags.
Compatibility policy
- Change taxonomy:
- Non-breaking (allowed in current version): adding optional fields, adding enum values (if clients ignore unknown), adding new endpoints/queries, widening constraints, adding defaultable request fields.
- Breaking (requires new major version): removing/renaming fields; changing field types or semantics; tightening validation; changing pagination, id formats, or error codes in incompatible ways.
- Semantic versioning for APIs:
- MAJOR (v1 → v2): breaking changes.
- MINOR (v1.x): additive, backward-compatible features.
- PATCH (v1.x.y): bug fixes, docs, performance; no contract change.
- Deprecation protocol:
- Mark deprecated: Deprecation: true and Sunset headers for REST; @deprecated(reason) for GraphQL; comments/options in protobuf and release notes for gRPC.
- Announce date and removal window (e.g., 90–180 days for external clients).
- Dual-run during migration; monitor usage; then remove.
Header examples for REST
Deprecation: true Sunset: Fri, 01 Nov 2026 00:00:00 GMT Link: <https://api.example.com/policies/versioning>; rel="deprecation-policy" X-API-Version: 1.8 Warning: 299 - "Field 'fullName' is deprecated; use 'name'"
Worked examples
1) Rename fullName → name in REST
Goal: rename response field fullName to name.
- Non-breaking path: keep fullName, add name. Document fullName as deprecated. Emit Warning header for 60–180 days. Later, remove fullName in v2 path (/v2/users).
- Rollout steps:
- Add name (v1.12). Keep fullName.
- Deprecate fullName with headers.
- Release /v2/users without fullName.
- Dual-run v1 and v2; monitor traffic.
- Sunset v1 after adoption threshold.
2) Add optional middle_name
Adding an optional response property is backward-compatible. No new version required. Ensure clients are tolerant of unknown fields. Document the field and update SDKs.
3) Enum evolution: status = {pending, active}
Adding paused is usually non-breaking if clients handle unknown values safely. Recommendations:
- Document that enum sets are open and clients must handle unknown values.
- If a client crashes on unknown values, provide a transitional header (e.g., X-Enum-Compatibility: strict) or a compatibility flag until they update.
4) gRPC message evolution
syntax = "proto3";
package user.v1;
message User {
int64 id = 1;
string name = 2; // deprecated: use given_name + family_name
string given_name = 3;
string family_name = 4;
}
// Rules:
// - Never reuse field numbers. Use 'reserved' for removed fields.
// - Add new fields with new tags; keep them optional.
// - For breaking wire changes, create user.v2 package.
Design checklist
- [ ] State your default compatibility policy (what changes are safe).
- [ ] Choose canonical version signal (path or header) and stay consistent.
- [ ] Define deprecation + sunset window with dates.
- [ ] Provide migration guidance and examples per change.
- [ ] Decide on monitoring: how you measure remaining v1 usage.
- [ ] Document GraphQL deprecation and gRPC protobuf rules.
- [ ] Plan SDK alignment and sample requests for each version.
Exercises
Do these now. They mirror the graded exercises below and prepare you for the quick test.
- Draft a versioning policy for a platform offering REST, GraphQL, and gRPC.
- Plan a safe rollout for a breaking change that removes a field and tightens validation.
Need a nudge?
- Prefer additive changes in-place; reserve majors for truly breaking changes.
- Use Deprecation/Sunset headers and a dual-run period.
- For gRPC, never reuse field numbers; use new package for majors.
Common mistakes
- Mixing version signals (some endpoints in path, some in header) causing confusion. Fix: pick one canonical method per API surface.
- Declaring a breaking change as minor. Fix: follow your taxonomy; if clients must change code, it is a major.
- No sunset dates. Fix: always set dates and communicate clearly.
- Removing deprecated fields too quickly. Fix: use data-driven thresholds and grace periods.
- Reusing protobuf field numbers. Fix: mark removed tags as reserved.
- GraphQL hard breaks. Fix: use @deprecated and allow time; consider a new graph name for majors.
Self-check
- Can you explain which changes are safe vs breaking with one sentence each?
- Can a client discover the current version and deprecation status from responses?
- Is there a written removal date and migration example?
Practical projects
- Build a small REST service with /v1 and /v2 for a users resource. Add Deprecation and Sunset headers and a metrics log for v1 usage.
- Create a GraphQL schema, deprecate a field, and add a resolver that supports both old and new shapes.
- Design a protobuf for Orders, simulate adding fields and reserving old tags, then publish a v2 package with a breaking rename.
Quick Test
Available to everyone; only logged-in users get saved progress.
Answer short questions to validate understanding of breaking vs non-breaking changes, version signals, and rollout steps.
Mini challenge
Your API returns price_cents as an integer. You want to return a structured price object with currency and amount. Propose a plan that minimizes client breakage. Include: current version handling, deprecation headers, v2 design, migration guide snippet, and how you will monitor adoption.
Who this is for
- API Engineers, Backend Engineers, and Tech Leads designing evolvable services.
- Mobile/web engineers consuming APIs who need to understand server change policies.
Prerequisites
- Basic HTTP knowledge (methods, status codes, headers).
- Familiarity with JSON/GraphQL or protobuf messages.
- Understanding of backward compatibility.
Learning path
- API style basics → Resource modeling → Versioning strategy (this lesson) → Deprecation & migration → Observability for change rollout.
Next steps
- Write a one-page versioning policy for your team and share it for review.
- Identify one upcoming change and classify it as minor or major with rationale.
- Implement deprecation headers in a staging endpoint and test clients.