Hacker News new | comments | show | ask | jobs | submit login
How to Version an API (fly.io)
215 points by goodroot 131 days ago | hide | past | web | 76 comments | favorite

I personally like to think about managing change well rather than versioning an entire API. In this, API designers provide instructions on how APIs will evolve over time, how features will be deprecated, and how this process will be communicated to API consumers. Change management also includes recommendations for client developers on how to evolve along with the API and build clients that don't fail when something as small as a JSON property is absent or added. You don't necessarily need version numbers to do this.

An API version says to me that one day the entire API may introduce a major change that is separate from the current API. If you plan to never introduce a major change like this, you may not need a "/v1" in the URL.

Kudos to the author for putting together a nice article :)

API consumers might also not be very flexible when it comes to adapting to API changes: some might have developed fragile/inflexible code that will go up in flames if you even add a new field to an object (leading to support calls and escalations), this is why depending on your domain you might need very strict versioning requirements.

Some APIs will not change a version number even if new objects/fields are added, others will, it really depends by what space you are in and your customer needs and it should be taken into account when designing your API.

I think you are right here. It takes two to tango, and if the clients are tightly coupled to the API, the best intentions for evolving slowly will fail. :)

Never say never. It's generally impossible to predict how an API will have to evolve over time in response to changing requirements.

That's true. However, I think this falls under YAGNI [0]. If you happen to get to a point where you have to change your an entire API rather than evolve what you have, you should consider it a new, separate API rather than instruct all client developers to plan for it up front. Plan for evolvability and handling changing requirements up front.

[0] https://martinfowler.com/bliki/Yagni.html

And how would you name this new API? Bob? Or version 2?

Name it whatever you want. Point is, you don't have to plan for such a drastic change when a separate domain/subdomain will solve it for you.

As an API consumer, clean cuts are much, much more manageable than a continuous stream of small breakages.

True. I would consider a continuous stream like that to not be helpful change management. But if you have to have continuous breaking changes like that, you would want clean cuts. My point is, find ways to evolve your API without breaking things.

If you're a Rails dev, take a look at VersionCake.

It allows you to version your json Jbuilder views by adopting the file naming convention (e.g. show.json.v1.jbuilder, show.json.v2.jbuilder, etc).

There's fallback to most recent, compatible version of the API.


I was like, Who's recommending VersionCake? But it's none other than Jim Jones!

Offtopic: I was amazed by the quality of the illustrations and was pleasantly surprised to find that the woman who makes the illustrations (https://twitter.com/annieruygt) is listed on the company's about page.

Annie's amazing!

The title is a bit misleading: the article is about HTTP-based API versioning, not just any kind of API. A more generic approach that would fit such a title is "semantic versioning" [1], which covers different questions.

[1] http://semver.org/

Yeah. People should stop saying API for a HTTP API. This is an application programming interface. Any application.

> Header versioning in the wild is primarily done via a customized Accept HTTP header.

Is this true? I haven't looked at very many web APIs, but I haven't run across anyone using Accept in this way, and it strikes me as a pretty bad mis-use of that header.

How is it a bad misuse? It's used to specify the MIME type. Why is it so much of a stretch to also specify which version of the MIME type to use? I personally think this is the cleanest and most unobtrusive way to version an API. I'd rather change that than have to change the URL all the the time. Not to mention that /api/vX/... is just ugly.

I don't understand how changing one or the other of these things is somehow easier, nor does it make sense to try to claim one or the other is "ugly"... seriously? Do you even see these requests? I just don't understand this thought process.

The Accept header specifies the acceptable MIME types for the response. It doesn't specify how the request itself is supposed to be interpreted. Picking between alternate MIME types with the Accept header is still supposed to give you the same content, just in different representations.

The proper solution here is to just define a new header, like X-API-Version (note: the article picked a header API-Version, but since this is a non-standard vendor-specific header it should be prefixed with X-).

A different version is just a different representation of the same resource, the Accept header is the correct way to do it.

If the only difference is the data returned, and not anything to do with how the request is actually interpreted, then I guess it works. But API versioning covers more than just changing the fields in the JSON response.

If the client knows the version to send, they can send a different request format as well and you'll know how to interpret it due to the version header; that header isn't just used for the response, it's an agreement about what the specific message format in both directions is. If he's looking for a version 5 response then parse it as a version 5 request. I don't see the problem? Now your media types and versions of those types can vary and grow without messing with your resource locations which can remain stable. Beyond that you can now version each resource independently rather than forcing the app as a whole to change.

Your problem is you're taking a header that's explicitly intended for selecting among multiple response types, and using it to control how the request itself is processed. That's simply incorrect. That's not what the Accept header is meant to do, and using it that way is confusing.

> that header isn't just used for the response, it's an agreement about what the specific message format in both directions is

No, it's not. The Accept header is strictly about the response.

A lot of the time, "breaking the API" means "breaking the response that comes back", which in turn means that the client doesn't know what to do with the response (can't parse it, missing fields, different schema etc.).

If I change my API so that "POST /widgets" does something different on the backend but returns the same response, then from a client compatibility perspective this isn't a breaking change.

Yep, you should also send a Content-Type if your request has a body.

> No, it's not. The Accept header is strictly about the response.

OK, if you want to be that strict about it, then you use both the ContentType and Accept headers to specify what version of the request and response you're making/wanting.

The Content-Type specifies the MIME type of the body content. So even that's not good enough for actually interpreting the URL or URL parameters. Content-Type would only work if all of your APIs go through the same URL and strictly use the body content for the actual request (which would be a pretty awful API).

Fair point; I still say if you're requesting version 5 response then the server can interpret the request as version 5 as well and act accordingly, they're necessarily linked regardless of the Accept header being about the response; it must necessarily imply the request URI version format.

I can see why you're saying that, but I still disagree that this is a valid usage of Accept, and at a minimum it's going to be confusing, because nobody expects the Accept header to affect interpretation of the request.

I'm also not sure why anyone would even want to use Accept for this, given that if you can set Accept, you can also set something like X-API-Version.

It's not only a valid usage of accept, it's simply necessary; you can't possibly process a request for a version of a response without assuming the request contains the necessary information said response requires. If you don't expect the Accept header asking for a specific version to affect the processing of the request, I think you really haven't thought it through as it necessarily must since the output response version must be correlated with the input request. If output version 5 requires a different set of input args, then your code for processing the output will have to assume those args present and thus will have to assume the request was formatted correctly for version 5 otherwise you'll have to throw an error saying the request was formatted incorrectly.

Yes, you could use a custom header, many do, but then you're stepping outside of the HTTP standard, the Accept header is supposed to define the response format, not some custom one you make up, and version is another facet of the response format and must imply the request format matches the required data of the requested output format.

...what? Your comment literally makes no sense to me.

> Yes, you could use a custom header, many do, but then you're stepping outside of the HTTP standard

No you're not. The HTTP standard allows for custom headers, and nothing in the HTTP standard restricts servers from using customer headers to influence how they process the request.

> Accept header is supposed to define the response format

Yes, like application/json. It's not supposed to define the request content, just the format, and it's definitely not supposed to affect the way the request is processed.

> version is another facet of the response format

No it's not. JSON is a response format. XML is a response format. "API version 5" is not a response format.

The mime-type has changed. How you interpret the mime-type has changed.

Because the version of the API is not the version of the MIME type. It's not like JSON itself came out with version 2 of the JSON specification.

Look at the mime-type declaration again - its not application/json. It is a representation of a resource in JSON format. The resource is the part thats changed, and interactions with that resource are defined as part of the mime-type.

I've seen some use a versioned media typed like GitHub [0] or add a version parameter like "vnd.example-com.foo+json; version=1.0" [1]. People may use this version for the entire API or for versions of that specific resource as well.

[0] https://developer.github.com/v3/media/ [1] http://blog.steveklabnik.com/posts/2011-07-03-nobody-underst...

Roy Fielding himself had an interesting interview [0] where versioning a REST API was one of the topics (spoilers: version is not in the url nor headers).

[0]: https://www.infoq.com/articles/roy-fielding-on-versioning/

> My point was that there is no need to anticipate such world-breaking changes with a version ID. We have the hostname for that. What you are creating is not a new version of the API, but a new system with a new brand.

I think Rich Hickey (the creator of Clojure) would agree. He said something to this effect in the "Spec-ulation" keynote at Clojure/conj 2016. https://www.youtube.com/watch?v=oyLBGkS5ICk

I'm going to have to disagree with him. If I need to break compatibility on a single resource, I don't want to mirror every API endpoint (except that one) to a new hostname. That becomes continually more and more complex and hard to keep sense of as changes progress.

The hostname thing might work for huge, world-changing re-writes, but for small, incremental changes, I think something like what GitHub does w/ the Accept header is far more appropriate.

His argument is for API discoverability. Client should be able to learn about available endpoints

[edit: ...and resources. And be able to comprehend resource record conventions]

I also don't see the point of API discoverability, at least, not programmatically. Sure, such a thing could be built, but I don't know what could be done w/ the results. Most API's I've ever worked on required domain knowledge in order to operate on (e.g., you had to understand the content — semantically — of the result. I.e., you needed to understand what a `applicaiton/vnd.bigcorp.widget.v2+json` was.)

I just don't see any generic operations w.r.t. discovery that are … useful.

I'm not trying to say that you shouldn't, e.g., use URLs in your resulting resources; e.g., if a query for a widget returns:

    "widget_name": "NextGen Enterprise Widget",
    "suppliers": "http://bigcorp.example.com/widget/123/suppliers"
That's fine. But you're still going to need to know how to parse the result of …/suppliers, or it's just opaque bytes.

Currently I structure my URLs like this:

The idea is that you write in the date that you wrote the implementation against the API. This is just like Stripe's API, but explicit rather than implicit. Then everything under that will continue to work as-is.

There's a few reasons I like to do this, but the main one is that I often struggle to decide whether a given change is really "worthy" of a major version change.

The only rule/caveat we have is: within a given client's implementation, you should only use one date at a time. Mixing them creates a small possibility of compatibility issues.

The initial /v1/ is just in case I decide to move away from the date thing at some stage.

I'm similar but just prefix like

I assume I won't make multiple versions in a year. I have a current system with /v2014, /v2016 and /v2017

I like to smash dates into all my version strings

Yes, human readable dates are very good at conveying a sense of obsolescence.

Mine are a bit more granular: /vyymm, e.g. /v1503, /v1610, /v1706. This should work well for this century ;-)

> 3: RESTful. It should speak the language of HTTP verbs: GET, PUT, POST, PATCH, UPDATE, DELETE.

I know it's slightly off-topic, but when one of the early points of an article contains an incorrect piece of information, that really makes me not want to read on further

Restful API's are a recent "fad" that, even though_do_ have their place in mainly basic CRUD apps, RPC still reigns very popular. See [1] for a pretty good explanation of REST-styled vs RPC-styled APIs.

[1] https://www.linkedin.com/pulse/rest-vs-rpc-soa-showdown-josh...

I've rarely seen a "pure" API that wasn't also very simplistic... most composite APIs of any complexity have things that work better as an RPC request (via HTTP POST) as well as other more restful endpoints.

Wrt your users, I think regardless of what scheme you use for API versioning it's a good idea to build libraries to access it – if you can afford it. [Go :), ]JS, PHP, C#, Java, Python, and Ruby, and you have most of the market covered. Then you can use semver for the libraries and the user doesn't care about how the actual endpoints look like – as an added bonus you can use more efficient encodings.

Example from a past life: https://github.com/lavab/api-client-js

Great, I was wondering how to version my library once I reach 1.0.

> 3: RESTful. It should speak the language of HTTP verbs: GET, PUT, POST, PATCH, UPDATE, DELETE.

Oh, it was talking about web interfaces. Oh well.

Is there a reason you can't use semver? Or was this just a quippy remark?

This article isn't even about semantic versioning. It is exclusively about web API design and versioning, and the web doesn't interest me.

API used to mean headers, types, Java interfaces… stuff programmers use to link code together. And now the web is hijacking the term to a point where regular APIs can't even be named. I mean, can web developers even think about this kind of API now?

I bet this explains part of the microservice madness we've seen lately. If the only kind of API you know goes through a web server, of course you'd use not-so-micro-services to modularise your application.

My point was, that if it came to versioning API library versions, then "just use semver" would seem to be the most appropriate answer. I understand the conflation between "web api" and "api" here, I feel the same when "Microsoft SQL Server" is referred to as simple "SQL Server"

I do intend to "just use semver". I was just hoping this article would give me something more.

> RESTful. It should speak the language of HTTP verbs: GET, PUT, POST, PATCH, UPDATE, DELETE

What in the heck is UPDATE? It sounds like a non-technical person found out REST had CRUD.

It's the verb you use when you want to receive a 400 with "BAD Request - Invalid verb"

There's nothing RESTful about using more than 2 HTTP verbs (GET for SAFE endpoints, and POST for UNSAFE ones). (Mind you, REST does require that you obey the semantics of HTTP for the verbs that you do use, standard or non-standard).

And then the API endpoints illustrated don't obey HATEOS. So there's nothing RESTful about the article either.

You can define your own HTTP verbs if you want

But failed to also add INSERT.

> When you construct an API, you receive scalability and load balancing benefits by decoupling your API and hosting it as its own backend

I've thought about doing this a couple of times but it seems like a lot of overhead if you need to have separate production instances running for each version of your API.

Is there a better approach to organize this in a more scalable manner?

The two options are have the different versions running on separate instances or have all versions in a single instance. The former lets you scale the versions independently. If v1 traffic drops 5x overnight then you can scale it down by 5x. The latter simplifies the deployment and management though. You just have to deploy one service and scale one service to a single amount of traffic.

Random thought, but would it make sense to give each client a unique base path for URL endpoints and let them instantiate new endpoints with different versions?




I was just thinking about this. Instead of versioning with an `Accept` header, or with a URL version, why not version off of an upgradable API key?

Facebook does this partly: Each “Application” has an associated API version which is the earliest API version that can be accessed (and IIRC the one used implicitly when no version is given inside the route).

What I've learned about API versioning:

1. Versioning an entire web service or RPC API is relatively expensive in terms of engineering resources. Wherever possible, avoid having more than one version of an API, in favor of a single API version that evolves gradually in backwards-compatible ways. Having multiple API versions is a recourse to employ when you've realized that the original API isn't ideal, and you need to break compatibility to get it right. Strive to avoid this.

2. Establish ground rules with your clients about what kind of changes will be considered backward-compatible: what kind of changes they are expected to tolerate. For example, adding new fields to existing structures is something that clients should tolerate from the service. Pay careful attention to enumerations, sum types, and exceptions.

3. Version upgrades are expensive. Upgrading from one API version to another typically requires engineering time from every single client application. Any time that clients spend on version upgrades is time taken away from more useful activities. The total cost of a version upgrade grows in proportion to the number of clients. Don't gratuitously release new API major versions; strive to minimize them.

4. As a service provider, it often seems convenient to just release a new API version rather than put in the hard work to provide a feature in a backward-compatible way. (I see this more often in libraries than in services, where it's difficult to know 100% for sure that your change to the library won't break anyone.) Resist this temptation. Just because the upgrade costs your clients pay are hidden from you doesn't mean they don't exist. Version upgrades are disruptive and frustrating.

5. It's sometimes possible to implement backwards-incompatible changes through a series of smaller backwards-compatible or non-disruptive changes. For example, say that you want to rename an API element -- normally this would be a backward-incompatible change. However, you can break the change up into multiple steps: first, add the new element with the desired name and announce that the old name is deprecated. Next, work with your clients to upgrade them all to the new name. Once clients have migrated, you can remove the old name without impact. (How hard this is and how long it takes depends on the situation; it varies from relatively easy to almost impossible. A key factor is your clients - the more you can coordinate with and influence your clients, the easier this is.)

6. Releasing changes in backward-compatible or non-impacting ways is especially useful because it reduces the need for a service provider and client to directly coordinate. The service provider can announce a feature or change and clients can update to account for it on their own timeline. The service provider can then implement any backward-incompatible steps once all clients are ready.

7. When running any kind of migration, such as renaming an element or moving clients from one major version to another, it's helpful to have statistics about what people are using. If your V1 API isn't getting any traffic because users have moved to V2, or alternatively if it's continuing to receive traffic, then that's important to know if you're planning to deprecate it.

8. Versioning can usually be added after-the-fact if it's needed. It's often not necessary to worry about it too much up-front. I have yet to run into a situation where the lack of a version field caused a problem (because its absence can be interpreted as Version=1).

9. Most of what I said above matters when you have a large number of clients, or when you have clients that are outside your influence. This is big company, big-usage thinking. If you're e.g. a small company building services for your own consumption, don't worry about API versioning until you begin to notice friction. Instead, focus on making changes in backwards-compatible ways. If you need to make a backward-incompatible change, then talk to the people whose software will be affected. There might be an easier way to manage the change than maintaining two parallel API versions.

We had just changed to a new company running our invoices. They were still developing their API and we wrote code to transfer our invoices to them for printing and distributing. They were expensive and there was starting to pop up new competition for them. But we had already working code with them so we could not justify the cost of rewriting it. Suddenly they deprecated the old API forcing us to rewrite. Or liberated us to change to a cheaper and better alternative.

Well, we took the opportunity to change provider and are very happy now :-)

Great points. Also make version tag explicit on both request and response.

How about systems like GraphQL where the client specifies the attributes to be returned? To deprecate an attribute, just mark it as deprecated and wait until no client is requesting that field.

> just

No. Marking something as deprecated helps during development. It doesn't help if it's a bunch of deployed code running in production that nobody has the time to maintain for third-party changes. That code will keep making those requests until they fail.

GraphQL inherently doesn't address versioning. You'd have to version each attribute and query separately by prefixing or suffixing their names. But then you'd lose the succinctness GraphQL users love it for.

The official answer is "Just add new attributes" but that means you're stuck coming up with new names for slightly different versions of existing attributes (or queries). If you try to "be bold" and make things up as you go along, your API will be littered with the equivalent of `2017-06-09_docs_final_v2_new_copy.pdf` if you take backwards compatibility serious.

Seeing how Relay is slowly turning GraphQL APIs into RPC APIs ("client sends complex query blob with parameters to server" has become "client sends query ID with parameters to server", server-side GraphQL is now an implementation detail only relevant during development), maybe the right question to ask is:

How do I version an RPC API?

You'll be waiting possible forever, clients tend to not change until forced to.

And why should they? They probably hired some developer to make the application and this developer might not be available any more. Are you forcing the user to change or are you liberating the user to move to a new company with similar services? If you want your customer to stay with you, make sure it is as frictionless as possible to stay with your service. Of course, if the change is because of security you have to be more clear about it but change just for change sake is dangerous.

Oh I agree, you should support them as long as you reasonably can if their business justifies the expense and complexity of maintaining multiple versions. At some point it eventually won't, and that's when you force them to upgrade or lose access.

Has anyone ever designed an API with subversioning on single calls?

So you'd have: service.com/api/v1/do_thing/v2/param

That way, you wouldn't have to try to snap new features to entire numbered revisions,and could open new parameters / results up to clients who are interested in the meantime. Seems like it would be the best of both worlds, but I've never had to design a serious API.

At my last job, we used https://retailmenot.github.io/shield/ to both handle versioning and our microservices. This lets you even entirely swap out the microservice between versions!

I go by this - breaking changes require versioning. And once I version, there is no time spent on backward compatibility. Time is money.

We do this at my job, what better way to screw over API users than to make them rewrite their integration every 2 years

article doesn't include "module", which I think is a big mistake, as it assumes a monolithic api for the entire business.

My opinion for a better structure:




"The Graph API has multiple versions available"

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | DMCA | Apply to YC | Contact