Aroma Store's v1 contract is practically closed: resources, methods, codes, representations and collections. And just as a contract is published, the real problem begins: the business changes and the API has to change with it, without breaking anyone. Aroma Mobile has versions installed on phones that nobody is going to update for months; SwiftShip has an integration written a year ago that works and that nobody wants to touch. This lesson teaches you to draw a precise line between changes you can make without warning and those you cannot, compares the five versioning strategies used in the industry, justifies Aroma Store's choice and designs the complete deprecation cycle, from the announcement to the shutdown.

Contents

  1. Why versioning is a last resort
  2. Backwards-compatible versus breaking changes
  3. Table of changes to the Aroma Store API
  4. Versioning strategies
  5. Comparison and Aroma Store's decision
  6. SemVer applied to APIs and its limits
  7. Versions living side by side
  8. Life cycle and deprecation
  9. The Deprecation, Sunset and Warning headers
  10. Strategies for not having to version

  1. Why versioning is a last resort

Publishing a v2 sounds like progress. In reality it is an invoice: two code bases to maintain, two sets of documentation, two test suites, two security surfaces, and a migration that has to be negotiated with every consumer. Well-known companies have spent a decade maintaining their v1 because switching it off turned out to be impossible.

That is why rule number one of versioning is: avoid needing it. Most changes can be made in a backwards-compatible way if the contract was designed with tolerance to change (02-01) and if you can draw a precise line between what breaks and what does not. That is exactly what the next section does.

  1. Backwards-compatible versus breaking changes

The operational definition, and the only one that is any use:

A change is backwards-compatible if a client written against the previous contract, and behaving correctly, carries on working without touching a single line.

Notice the nuance "behaving correctly": a client that breaks because it iterated over the JSON's fields assuming there were exactly five is not your fault, as long as the documentation warned that new fields may appear. Hence the robustness principle and the tolerant reader from 02-01 are not soft advice: they are the condition that makes evolution without versioning possible.

2.1. The general rule

  • Adding is almost always safe (fields, endpoints, output enumeration values, optional parameters).
  • Removing, renaming or restricting almost always breaks (fields, endpoints, accepted values, status codes).

2.2. The input/output asymmetry

This is what confuses people most, and it is worth thinking through slowly: adding a value to an enumeration is not the same thing in the response as in the request.

  • On output (server responses): adding status: "returned" is a potentially breaking change for clients that do a switch with no default case. It is considered acceptable only because the documentation warns from day one that enumerations grow.
  • On input (client requests): accepting a new value in roast is completely safe, because nobody was sending it before.

The mirror image, for the same reason inverted: relaxing an input validation is safe; tightening it breaks. If today you accept comments of 5,000 characters and tomorrow you limit them to 500, clients that were working stop working.

  1. Table of changes to the Aroma Store API

Change Does it break? Why
Adding the averageRating field to a coffee's response No Clients that ignore it carry on unchanged (tolerant reader)
Adding the /v1/subscriptions endpoint No Nobody was calling it
Adding the optional filter ?available=true No The behaviour without the parameter is untouched
Adding the return link to an order's _links No Additive, and we already warned that the links depend on the state
Accepting a new value in roast on input No Nobody was sending natural before
Adding the value returned to an order's status Almost: documented as expected It breaks anyone who did not foresee new values; it is announced in advance
Renaming price to priceEuros Yes Every client reading price gets undefined
Removing the stock field from the response Yes A piece of data that was being used disappears
Changing stock from a number to a string ("120") Yes stock > 0 stops behaving the same; "0" is truthy in JavaScript
Changing priceEuros from euros to cents (1450) Yes, and of the worst kind It does not fail: it shows prices a hundred times higher. A silent change is worse than a noisy one
Making the origin field mandatory when creating a coffee Yes Requests that worked now return 400
Limiting comment from 5,000 to 500 characters Yes Tightening a validation breaks whoever was near the edge
Removing the value pending_payment from status Yes Clients have it in their conditionals
Changing POST /orders from 201 to 200 Yes Clients that check === 201 fail; and Location disappears too
Changing the error code coffee_not_found to not_found_coffee Yes The code is contract; it is compared in the client's code (02-04)
Changing the text of an error's message No It was documented that message is for humans and can change
Changing the default ordering of /coffees from name to -createdAt Yes It may look harmless, but it changes what page 1 shows and it was documented
Reducing the maximum limit from 100 to 50 Yes Valid requests start returning 400
Raising the maximum limit from 100 to 200 No Relaxing a limit is safe
Changing the id format from cof_001 to cof_01HQ8ZK… No It was documented as an opaque string (02-02); it only breaks whoever was parsing it, and they were warned
Returning null in a field that never was Yes The client does coffee.origin.toUpperCase() and blows up
Migrating /coffees from offset to cursor by removing offset Yes A published parameter is not withdrawn without a version
Adding the Aroma-RateLimit-Remaining header No New headers are ignored automatically
Fixing a 500 that now returns 400 No (considered a fix) You were breaching your own contract

The last row points at a genuine grey area: fixing a bug can break whoever depended on the bug. It is documented in the changelog, it is announced, and in general it is considered a non-breaking change. But if the wrong behaviour had been there for two years, it may have become contract de facto: it has to be looked at case by case.

  1. Versioning strategies

4.1. Version in the path

https://api.aromastore.example/v1/coffees
https://api.aromastore.example/v2/coffees

It is the most used in the industry: Twitter/X, GitHub (for years), Stripe in its base URL, almost every corporate API.

# Visible, copyable, testable from a browser
curl https://api.aromastore.example/v1/coffees

In favour: visible at a glance; it can be tested with a browser or with curl without headers; routing is trivial (a prefix); logs and metrics separate versions effortlessly; the documentation's examples are self-contained.

Against: REST purists object that the URI should identify the resource, not its format, and that /v1/coffees and /v2/coffees are "the same coffee" with two different URIs; on top of that it versions the whole API even when only one resource changes, and the _links clients have stored are pinned to a version.

4.2. Version in a query param

https://api.aromastore.example/coffees?version=2

In favour: the base URI is unique; it is easy to test; you can give it a default value.

Against: it gets mixed up with the business parameters (filters, sorting, pagination), it is easily lost when copying URLs, it complicates caching and it makes it ambiguous which version is served if the parameter is missing.

4.3. Version in a custom header

GET /coffees HTTP/1.1
Aroma-Version: 2

In favour: the URIs stay clean and stable; it allows granular versioning.

Against: invisible. You cannot paste a link into a ticket and expect it to reproduce the problem; testing in a browser is impossible without tooling; caches need Vary: Aroma-Version and many gateways ignore it; and you have to decide what happens if it is not sent.

4.4. Version in the media type

GET /coffees HTTP/1.1
Accept: application/vnd.aromastore.v2+json

It is the most correct option from REST's point of view: the version belongs to the representation, and the representation is negotiated with Accept (02-05). GitHub used it for years (application/vnd.github.v3+json).

In favour: theoretically impeccable; it uses a standard HTTP mechanism; it allows versioning resource by resource.

Against: the hardest to use. Nobody remembers the string by heart; curl requires an explicit header; many tools and generated clients handle it badly; and just like the custom header, it is invisible in the URL.

4.5. Versioning by date

GET /coffees HTTP/1.1
Aroma-Version-Date: 2026-03-14

This is Stripe's style: each account is anchored to the version in force on the day it integrated, and the server applies chained transformations to adapt the current response to the shape expected on that date.

In favour: there are no traumatic jumps from v1 to v2; changes are introduced continuously; every consumer migrates when it wants to; it is the one that scales best on large public APIs.

Against: high complexity. It demands maintaining a well-tested chain of transformations and considerable engineering discipline. It is an excellent pattern for a company whose product is the API, and disproportionate for almost everyone else.

  1. Comparison and Aroma Store's decision

Criterion Path Query Header Media type Date
Visibility High High Low Low Low
Ease of testing (curl, browser) Very high High Low Very low Low
REST purity Low Low Medium High Medium
Granularity (per resource) Low Low High High High
Ease of routing and deployment Very high Medium Medium Low Low
Caching and intermediaries Simple Medium Needs Vary Needs Vary Needs Vary
Implementation complexity Low Low Medium Medium High
Progressive migration Low Low Medium Medium Very high
Who uses it Twitter/X, most Simple APIs Azure, some others GitHub (historically) Stripe

Aroma Store's decision: the version in the path (/v1).

The reasons, in order of weight:

  1. Visibility and support. When SwiftShip raises an incident and pastes a URL, we will know exactly what they are talking about. With headers, half the tickets start with "which version were you using?".
  2. Cost of implementation and operation. A path prefix is routed, deployed, measured and switched off with standard tooling. With four consumers and a small team, theoretical purity does not pay off.
  3. Teaching and documentation. Every curl example in the documentation works copied and pasted, with no hidden headers.
  4. Consistency with what has already been decided. The base URL with /v1 has been fixed since module 1 and published to the four consumers.

And the associated decisions, which are also contract:

  • Only the major number is versioned: /v1, /v2. Never /v1.2. Minor changes are backwards-compatible by definition and need no new URL.
  • The version is mandatory in the path. There is no https://api.aromastore.example/coffees without a version that "points to the latest": a client that does not choose a version ends up broken the day the latest one changes.
  • All resources share a version. The whole API steps up at once, even if only orders change. It simplifies the reasoning at the cost of some granularity.

  1. SemVer applied to APIs and its limits

SemVer (semantic versioning) defines MAJOR.MINOR.PATCH:

Component When it goes up Example in Aroma Store
MAJOR A breaking change Renaming price to priceEuros
MINOR New, backwards-compatible functionality Adding /v1/subscriptions
PATCH A fix with no contract change Fixing a VAT calculation

Applied to an HTTP API, SemVer has three limits worth being clear about:

  1. Only the major number appears in the URL. A consumer does not care whether it is using 1.4.2 or 1.7.0: the contract it sees is the same. Minor and patch live in the changelog, not in the path.
  2. There is no "installation" to pin. With a library, the consumer decides when to upgrade; with a hosted API, the server upgrades for everyone at once. That is why minor changes have to be rigorously backwards-compatible: there is no going back for the client.
  3. The major/minor boundary is negotiated. Adding an enumeration value on output (section 2.2) is, technically, potentially breaking; declaring it MAJOR would force you to publish a v2 every quarter. It is documented as expected and treated as MINOR with a prior announcement. Write that policy into the style guide, because it is the decision that prevents the most arguments.

Aroma Store therefore maintains two numberings: the URL version (v1) for the contract, and the internal semantic version (1.7.0) in the changelog and in OpenAPI's info.version field (02-08).

  1. Versions living side by side

When v2 arrives, the two coexist for a while. The practical questions:

How many versions to maintain?

Two at most: the current one and the previous one under deprecation. Three live versions is a sign that the previous migration never finished, and the cost grows more than linearly: every security fix and every business change has to be applied to all of them.

What does it really cost?

Cost Detail
Code Routes, transformations and, sometimes, duplicated business logic
Tests The whole suite, twice (03-08)
Documentation Two complete and consistent references
Support Twice the number of cases in every incident
Security Every patch, applied and verified twice
Data v1 may need fields that v2 no longer uses

How are they routed?

The usual pattern, and the one Aroma Store will use in module 3: a single application with two presentation layers over shared business logic.

graph TD
    C1["Aroma Mobile 3.x"] --> R{"Routing<br/>by prefix"}
    C2["Shop SPA"] --> R
    C3["SwiftShip"] --> R
    R -->|"/v1/*"| V1["v1 layer<br/><i>transforms to the old contract</i>"]
    R -->|"/v2/*"| V2["v2 layer<br/><i>current contract</i>"]
    V1 --> N["Business logic<br/>and data<br/><b>shared</b>"]
    V2 --> N

The key is not to duplicate the business logic. v1 is implemented as an adaptation layer over the current model: it renames fields, trims what did not exist, calculates what was removed. Duplicating the whole service guarantees that the two versions diverge in behaviour and that bugs appear which only happen on one of them.

When the transformation stops being possible —because the data model genuinely changed— that is the signal that v1 should be switched off, not that the system should be duplicated.

  1. Life cycle and deprecation

Every version goes through four phases:

graph LR
    A["<b>Current</b><br/>the recommended version"] --> B["<b>Deprecated</b><br/>it works, but it warns"]
    B --> C["<b>Sunset announced</b><br/>shutdown date fixed"]
    C --> D["<b>Switched off</b><br/>410 Gone"]

Aroma Store's timetable, written into the documentation before publishing v1 —because withdrawal conditions are announced at the start, not when they have already become inconvenient:

Milestone Timing What happens
Publication of v2 Day 0 v1 remains current and supported
Deprecation of v1 Day 0 A Deprecation header on every v1 response; changelog and email to consumers
Reminders Months 3, 6, 9, 11 Email to the consumers still calling, with their usage figures
Read-only (optional) Month 11 Writes on v1 return 410; reads carry on
Shutdown Month 12 The whole of v1 responds 410 Gone with a link to the migration guide

A minimum of 12 months for external consumers such as SwiftShip. For our own clients (SPA, panel) the deadline can be shortened because we control the deployment, but not for Aroma Mobile: there are users who do not update the app in a year, and that is the consumer that really sets the timetable.

Communication

A deprecation that lives only in HTTP headers is a deprecation nobody reads. The complete package:

  1. Headers on every response (section 9) — for the software.
  2. A changelog with the date, the reason and a field-by-field migration guide — for the developer investigating.
  3. A direct email to the technical leads of every identified consumer — for the human who decides.
  4. A notice on the developer portal (05-06).
  5. Segmented reminders: only to whoever is still using the old version, with their concrete usage data. A generic email gets ignored; "we recorded 12,400 calls from you to /v1 this month" does not.

Metrics: knowing who is still there

Nothing gets switched off without data. You have to measure, by version and by consumer:

Metric What for
Requests per version per day See the migration curve and decide whether the deadline is realistic
Requests per version and API client Know who to phone
Most used v1 endpoints Prioritise the migration guide by what is genuinely used
Last access per client Detect zombie integrations that may no longer matter
Errors on v2 after migrating Discover that the migration is going badly before anyone complains

This requires identifying every consumer, which is achieved with each one's API key or token (04-03) and with the observability from 04-07. Without consumer identification there is no possible deprecation: only blind shutdowns.

The shutdown

When the date arrives, v1 responds:

HTTP/1.1 410 Gone
Content-Type: application/json
Link: <https://docs.aromastore.example/migration-v1-v2>; rel="deprecation"

{
  "error": {
    "code": "api_version_retired",
    "message": "Version v1 of the API was retired on 2027-03-14. Migrate to /v2.",
    "details": [
      { "migrationGuide": "https://docs.aromastore.example/migration-v1-v2" }
    ]
  }
}

410 Gone and not 404: the resource existed and has been deliberately removed (02-04). And v1 is not redirected to v2 with a 301: the contracts are different, so the client would receive a response with another shape and would fail confusingly. A clear error is better than a misleading success.

  1. The Deprecation, Sunset and Warning headers

The standard lets you announce the withdrawal within the protocol itself, so that software can find out without reading an email.

HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: @1773484200
Sunset: Sun, 14 Mar 2027 00:00:00 GMT
Link: <https://api.aromastore.example/v2/coffees>; rel="successor-version",
      <https://docs.aromastore.example/migration-v1-v2>; rel="deprecation"

{ "data": [ ], "total": 137 }
Header Standard What it says
Deprecation RFC 9745 That the resource is deprecated, and since when (a date with @ and epoch seconds, or true)
Sunset RFC 8594 The exact date from which it will stop responding, in HTTP date format
Link with rel="successor-version" RFC 8288 Where the replacement is
Link with rel="deprecation" RFC 9745 Where the explanation and the migration guide are
Warning RFC 7234, obsolete Readable warnings; removed in RFC 9111: do not use it in new designs

An important detail that gets forgotten: these headers can be used without publishing a new version, on one specific resource or field. If Aroma Store is going to withdraw /v1/customers/{id}/preferences and replace it with something else, that particular endpoint can carry Deprecation and Sunset while the rest of v1 stays perfectly healthy.

And a practical recommendation for consumers, worth including in the documentation: log a warning in your logs when these headers arrive. It is the cheap way of finding out about a deprecation without depending on somebody reading the right email.

  1. Strategies for not having to version

We come back to where we started: the best new version is the one you do not need. Five concrete techniques.

10.1. Optional, additive fields

Add instead of changing. When a field has to be replaced, both coexist during the transition:

{
  "id": "cof_001",
  "priceEuros": 14.50,
  "price": 14.50
}

price is documented as deprecated, with a withdrawal date, and disappears in v2. It costs you duplicating one piece of data for a few months; it saves you a whole version.

10.2. Tolerant reader

This is the consumer's responsibility, and it has to be documented and repeated:

// ✗ Fragile: it breaks with any new field or any new value
const { id, name, priceEuros, stock, roast } = coffee;
switch (order.status) {
  case "pending_payment": showPayButton();  break;
  case "paid":            showInvoice();    break;
  case "shipped":         showTracking();   break;
}

// ✓ Tolerant: it ignores the unknown and has a default case
const name = coffee.name ?? "Unnamed";
switch (order.status) {
  case "pending_payment": showPayButton();  break;
  case "paid":            showInvoice();    break;
  case "shipped":         showTracking();   break;
  default:                showGenericStatus(order.status);
}

The second version survives the day status: "returned" turns up. A tolerant client is what turns "adding" into a safe operation, and that is why Aroma Store documents it as an integration requirement, not as advice.

10.3. Feature flags and progressive rollout

A new behaviour is enabled first for one specific consumer, measured and then generalised. It lets you validate a doubtful change with the SPA (which we control) before exposing it to SwiftShip. Careful: a flag that stays forever is a version in disguise; every flag needs a withdrawal date.

10.4. Expansion and sparse fieldsets

Already designed in 02-05: expand and fields absorb a good share of change requests ("we need the customer's data inside the order") without touching the contract, because the generic mechanism was already in place.

10.5. New resources instead of changed resources

If /coffees has to change radically, sometimes the right answer is not a complete v2 but a new resource with a name of its own (/catalogue, /products) that lives alongside the old, now deprecated, one. You pay with two names for similar concepts, and you get paid in not versioning the whole API for a single resource.

Common Mistakes and Tips

  • Versioning out of habit. Publishing v2 because "it is time" doubles the cost without adding value. Version only when a breaking change is unavoidable.
  • Believing that adding a field never breaks. That is true for tolerant clients; with strict schema validation on the client, it breaks. That is why you have to document that the API may add fields.
  • Changing a field's meaning without changing its name. The worst possible change: it does not fail, it lies. priceEuros switching to cents multiplies prices by a hundred silently.
  • Not versioning the error contract. Error codes are contract just as much as fields: renaming them breaks clients (02-04).
  • Keeping three or four versions alive. It is a symptom, not a virtue: it means no migration was ever completed.
  • Switching off with no data and no warning. Without per-consumer metrics and an announced deadline, the shutdown is a serious incident with your partner.
  • Redirecting v1 to v2 with a 301. The contracts are different: the client will get a 200 with a shape it does not expect and will fail incomprehensibly.
  • Tip: write the changelog from day one. It is the cheapest artefact and the one consumers are most grateful for (02-08).
  • Tip: apply the "frozen client test". Faced with each change, ask yourself: would the version of Aroma Mobile installed a year ago still work? If the answer is no, it is breaking.

Exercises

Exercise 1: classify the changes

For each proposed change to Aroma Store's v1, state whether it is backwards-compatible or breaking and justify it. If it is breaking, propose an alternative that is not.

  1. Adding averageRating and reviewCount to the coffee representation.
  2. Renaming tastingNotes to tasteNotes.
  3. Adding the returned status to orders.
  4. Ceasing to return email in the customer representation for privacy reasons.
  5. Accepting PATCH with application/json-patch+json, in addition to Merge Patch.
  6. Changing totalEuros from 29.00 to "29.00" (a string) to avoid floating-point problems.
  7. Making Idempotency-Key mandatory on POST /coffees/{id}/reviews.
  8. Lowering the maximum limit from 100 to 50 because of performance problems.

Exercise 2: design a migration

Aroma Store needs to support several currencies. The current shape is:

{ "priceEuros": 14.50 }

And the desired one:

{ "price": { "amount": "14.50", "currency": "EUR" } }

Design the complete migration: is it breaking? Can v2 be avoided? What is published and when? Which headers are sent and what is communicated to each of the four consumers?

Exercise 3: plan the withdrawal of v1

Six months have passed since v2 was published and these are the v1 usage figures:

Consumer Requests/month to v1 Requests/month to v2 Last access
Web shop SPA 0 4,200,000
Aroma Mobile 890,000 3,100,000 Today
Internal panel 12,000 45,000 Today
SwiftShip 61,000 0 Today
Unknown client api_key_7f2 340 0 4 months ago

Decide whether v1 can be switched off in month 12 and draw up the action plan for each consumer.

Solutions

Solution 1

# Verdict Justification and alternative
1 Backwards-compatible Additive fields; clients that do not know about them ignore them
2 Breaking Every client reading tastingNotes gets undefined. Alternative: return both fields, document tastingNotes as deprecated with Sunset, and remove it in v2
3 Almost breaking, accepted It breaks anyone without a default case, but the documentation warns that enumerations grow. It is announced in the changelog in advance and communicated to consumers
4 Breaking A piece of data in use disappears. Alternative: stop returning it only to consumers without permission for personal data (04-03), which is an authorisation change rather than a contract change; and for the rest, deprecate it with a deadline
5 Backwards-compatible Widening the accepted input formats is relaxing, not restricting: nobody was sending JSON Patch before
6 Breaking It changes the type: totalEuros * 2 stops working and numeric comparisons fail. Alternative: add totalEurosText as a new field and migrate gradually, or leave it for v2
7 Breaking Requests that worked start returning 400. Alternative: accept it as optional, warn for months that it will become mandatory, measure how many clients already send it and make it mandatory in v2
8 Breaking Valid requests turn into 400. Alternatives: optimise the query; keep 100 and limit per consumer with rate limiting (04-04); or announce the reduction with a long deadline and measure how many use more than 50

Solution 2

Is it breaking? Yes, with no qualifications: priceEuros would disappear and change type (number → object). Any client that displays prices breaks, and in the worst case it displays [object Object].

Can v2 be avoided? Yes, with the field coexistence technique, and this is the right answer:

Phase 1 (month 0). price is added without removing anything:

{
  "id": "cof_001",
  "priceEuros": 14.50,
  "price": { "amount": "14.50", "currency": "EUR" }
}

A backwards-compatible change: old clients carry on reading priceEuros; new ones use price. As long as there are only euros, the two fields coexist without ambiguity. It is published in the changelog, priceEuros is documented as deprecated and the equivalence is explained.

Phase 2 (months 0-12). Responses with field-level headers and usage tracking:

Deprecation: @1773484200
Sunset: Sun, 14 Mar 2027 00:00:00 GMT
Link: <https://docs.aromastore.example/migration-price>; rel="deprecation"

You measure which consumers are still reading priceEuros —which in practice means asking them, because the server cannot see which fields the client uses: this is where fields= from 02-05 is useful as an indicator.

Phase 3. When the first non-euro currency appears, priceEuros stops being representable and that is when v2 is born, removing the old field. In other words: v2 is postponed until the business justifies it, it is not triggered by a change of shape.

Communication per consumer:

Consumer Action
Shop SPA Immediate migration: we control it and it ships in days
Aroma Mobile Migrate in the app's next release; 12 months of coexistence have to be assumed because of old installations
Internal panel Immediate migration
SwiftShip A formal email with the migration guide and the date; it does not consume prices, so it probably is not affected, but it is informed all the same

Solution 3

Verdict: it cannot be switched off in month 12 without prior work. There are two serious blockers and one minor one.

Consumer Diagnosis Action plan
SPA 100% migrated Nothing
Aroma Mobile 890,000 calls/month: there are old versions installed on phones. Publishing a new release of the app is not enough Force the update from within the app (a blocking notice), measure the distribution of installed versions and do not switch off until the curve falls below the agreed threshold. This is the main blocker
Internal panel 12,000 calls/month: there are screens still unmigrated An audit of the v1 endpoints in use and migration; it is internal, so it is a matter of planning the work. Deadline: month 8
SwiftShip 0 calls to v2: it has not started migrating. The most dangerous blocker, because it is external and we do not control its timetable Immediate direct contact with their technical team, a specific migration guide, a test environment and a date committed in writing. If they cannot meet month 12, a bounded extension is negotiated for their API key only, with a new, signed date
api_key_7f2 340 calls, no activity for 4 months: a zombie integration or a forgotten script Try to identify the owner from the sign-up data; if there is no reply within 30 days, warn about the withdrawal and switch off on the planned date. It must not drive the plan

Revised plan: keep the deprecation date, but set the shutdown at month 12 conditional on two measurable milestones: (1) that Aroma Mobile v1 falls below 2% of total traffic, and (2) that SwiftShip confirms its migration in writing. Between months 9 and 12, monthly reminders with concrete figures; in month 11, v1 in read-only mode to force forgotten integrations into the open; and a one-hour shutdown rehearsal (brownout) in month 10, announced in advance, which is the most effective technique for flushing out the consumers nobody knew existed.

Conclusion

Versioning is expensive, so the real goal is to need it as little as possible. You now know how to draw a precise line between a backwards-compatible change and a breaking one —adding is safe, removing, renaming, restricting and changing types is not, with the key asymmetry between input and output— and you know the worst category of all: the silent change that does not fail but lies. You have compared the five versioning strategies and you know why Aroma Store versions in the path with /v1, prioritising visibility and operational cost over the REST purity of the media type. And you have the complete life cycle: two coexisting versions at most over shared business logic, twelve months of notice for external consumers, Deprecation and Sunset headers with Link to the successor and to the migration guide, per-consumer metrics so you know who to call, and a shutdown with 410 Gone instead of a misleading redirect.

With this, the v1 contract is complete and it also has an evolution policy. What is missing is the piece that makes it usable by other people: telling them about it. In the next and final lesson of the module, 02-08 API Documentation, we will see why documentation is part of the product, which kinds of document serve which reader, what each endpoint's reference must include, and what changes when the contract is written in a machine-readable format such as OpenAPI. We will close by taking stock of Aroma Store's complete contract and preparing for the jump to module 3, where it is finally implemented.

REST API Course: Principles of Designing and Developing RESTful APIs

Module 1: Introduction to RESTful APIs

Module 2: Designing RESTful APIs

Module 3: Building RESTful APIs

Module 4: Best Practices and Security

Module 5: Tools and Frameworks

Module 6: Case Studies and Projects

© Copyright 2026. All rights reserved