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
- Why versioning is a last resort
- Backwards-compatible versus breaking changes
- Table of changes to the Aroma Store API
- Versioning strategies
- Comparison and Aroma Store's decision
- SemVer applied to APIs and its limits
- Versions living side by side
- Life cycle and deprecation
- The
Deprecation,SunsetandWarningheaders - Strategies for not having to version
- 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.
- 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 aswitchwith 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
roastis 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.
- 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.
- Versioning strategies
4.1. Version in the path
It is the most used in the industry: Twitter/X, GitHub (for years), Stripe in its base URL, almost every corporate API.
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
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
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
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
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.
- 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:
- 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?".
- 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.
- Teaching and documentation. Every
curlexample in the documentation works copied and pasted, with no hidden headers. - Consistency with what has already been decided. The base URL with
/v1has 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/coffeeswithout 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.
- 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:
- Only the major number appears in the URL. A consumer does not care whether it is using
1.4.2or1.7.0: the contract it sees is the same. Minor and patch live in the changelog, not in the path. - 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.
- 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
v2every 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).
- 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.
- 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:
- Headers on every response (section 9) — for the software.
- A changelog with the date, the reason and a field-by-field migration guide — for the developer investigating.
- A direct email to the technical leads of every identified consumer — for the human who decides.
- A notice on the developer portal (05-06).
- 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
/v1this 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.
- The
Deprecation, Sunset and Warning headers
Deprecation, Sunset and Warning headersThe 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.
- 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:
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
v2because "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.
priceEurosswitching 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
v1tov2with a301. The contracts are different: the client will get a200with 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.
- Adding
averageRatingandreviewCountto the coffee representation. - Renaming
tastingNotestotasteNotes. - Adding the
returnedstatus to orders. - Ceasing to return
emailin the customer representation for privacy reasons. - Accepting
PATCHwithapplication/json-patch+json, in addition to Merge Patch. - Changing
totalEurosfrom29.00to"29.00"(a string) to avoid floating-point problems. - Making
Idempotency-Keymandatory onPOST /coffees/{id}/reviews. - Lowering the maximum
limitfrom 100 to 50 because of performance problems.
Exercise 2: design a migration
Aroma Store needs to support several currencies. The current shape is:
And the desired one:
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:
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
- What Is an API?
- History and Evolution of APIs
- HTTP Fundamentals for APIs
- Basic Principles of REST
- The Richardson Maturity Model and HATEOAS
- REST vs. SOAP
- REST Compared with GraphQL, gRPC and Webhooks
Module 2: Designing RESTful APIs
- RESTful API Design Principles
- Resources and URIs
- HTTP Methods
- HTTP Status Codes
- Representations, Headers and Content Negotiation
- Filtering, Sorting, Pagination and Search
- API Versioning
- API Documentation
Module 3: Building RESTful APIs
- Setting Up the Development Environment
- Building a Basic Server
- Handling Requests and Responses
- Input Data Validation
- Persistence and the Data Access Layer
- Authentication and Authorisation
- Error Handling
- Testing and Validation
Module 4: Best Practices and Security
- API Design Best Practices
- Security in RESTful APIs
- OAuth 2.0 and OpenID Connect in Practice
- Rate Limiting and Throttling
- CORS and Security Policies
- HTTP Caching and Performance
- Observability: Logs, Metrics and Traces
Module 5: Tools and Frameworks
- Postman for API Testing
- Swagger and OpenAPI for Documentation
- Popular Frameworks for RESTful APIs
- Contracts, Mocks and Automated API Testing
- Continuous Integration and Deployment
- API Gateways and Developer Portals
