Cover image by Kevin Dooley, on Flickr
Greenfield projects are a beautiful thing. We don't have any legacy burdens and can choose technology and design of our APIs as we please, but the honeymoon phase doesn't last forever, and sooner or later we have to ship a first stable version of our API.
Before we hit this milestone, we should think about how we update our API in the future.
Everyone has to say something about API versioning. Some think we shouldn't version our APIs explicitly and evolve them over time, while always staying backward compatible with every client.
Others say it should be done explicitly to make it easy for our customers to adapt to new versions while telling them that their older clients are using soon to be deprecated functionality.
Moreover, finally, when we decided on versioning, there are different approaches to how versioning can be implemented, each with their pros and cons.
In this article, I explain the different approaches for maintaining backward compatibility and their impact on the development lifecycle of our APIs.
The focus is on GraphQL and RESTful APIs.
Creator Opinions
There are two ways to maintain backward compatibility with existing clients.
- Create different API versions
- Evolve one API version
Let's read what the creators of REST and GraphQL have to say about this topic.
To get an idea of what the creator of the REST architecture had in mind, we can read Roy Fielding's dissertation from 2000.
He doesn't explicitly mention versioning (only in the context of HTTP versions and document versions, which are both on a different abstraction level than our API version). Instead, he talks about the fact that different clients can require different media-types for a given representation.
He writes:
The data format of a representation is known as a media type ... Some media types are intended for automated processing, some are intended to be rendered for viewing by a user, and a few are capable of both.
moreover:
All REST interactions are stateless. That is, each request contains all of the information necessary for a connector to understand the request, independent of any requests that may have preceded it.
A representation is the data we get returned from the API.
A connector is a client or server library.
While this doesn't voice any opinion on versioning, at least it tells us that there has to be some information encoded in our architecture that two connectors understand each other.
In my opinion, this can be interpreted for either one of the two approaches, versioning, and evolution.
- If we want versions for whatever reason, we have to encode it in our requests
- If we want evolution, we defined that new features won't impact older clients and so we don't need to encode them in our requests.
So, what do the GraphQL creators have to say about this?
While there's nothing that prevents a GraphQL service from being versioned just like any other REST API, GraphQL takes a strong opinion on avoiding versioning by providing the tools for the continuous evolution of a GraphQL schema.
Their opinion is, GraphQL's query language is flexible enough to let older clients ignore the new features added so they won't break with changes, while also stating that they don't have anything else to say about versioning a GraphQL API like any other REST API.
To summarize it, GraphQL APIs should prefer evolution over versioning, but overall, nobody has an absolute opinion about either approach. So the next step here is to check the pros and cons of them.
Versioning
So why should we version our API? Here another quote from the GraphQL docs:
Why do most APIs version? When there's limited control over the data that's returned from an API endpoint, any change can be considered a breaking change and breaking changes require a new version.
The rationale behind this is, if you don't define breaking and non-breaking, everything is potentially breaking.
From everything being potentially breaking follows the need to version all changes we make to the API.
For example, we could add a new field to a JSON response and usually this wouldn't lead to a problem, but somehow somewhere one client used the number of fields in a JSON object to get the right data. We could also decide to send too much data, and suddenly one client can't keep up anymore.
So one big pro of versioning is telling everyone that they get exactly the data in a way they are used to.
Then there is deprecation.
While we can construct theoretical edge cases in which an ordinarily non-breaking change breaks something, there are enough cases in which a change breaks clients, one of them being the removal of data from responses.
For example, we use firstName
and lastName
and now want to go international and simply send a name
while also getting rid of the old format. Every client that accesses the non-existent fields will probably break.
With versioning, we can tell developers of clients to our APIs that they need to consider a change of format if they want to use our next version while being able to keep other clients working if they don't switch to the new version.
So the assumption is if we don't know what's a breaking change and what not, we need some way to inform our users about these changes so they can make their own decision.
If we would push out changes without any further notice, our users could end up in maintenance hell, where their clients break all the time for no apparent reason.
Evolution
What are the reasons for going with an evolutionary approach instead of versioning all changes?
Let's look at what the GraphQL creators say:
GraphQL only returns the data that's explicitly requested, so new capabilities can be added via new types and new fields on those types without creating a breaking change. This has led to a common practice of always avoiding breaking changes and serving a versionless API.
REST is an architectural style, GraphQL is a specification, and by its nature, a specification is much stricter than an architectural style.
While it is possible to follow many of the GraphQL principles with a REST API, often parts of a REST API remain unspecified. GraphQL tries to leverage this spec by defining that adding to an API doesn't break backward compatibility.
If we would specify that our REST API reserves the right to add data to a representation in the future and clients should be resilient to this practice, we also end up with a defined type of non-breakable change that can be used to evolve our API.
This approach at least allows for the addition of features, but what about removal?
GraphQL defines a @deprecated
directive to mark fields or enums that should no longer be used.
However, the GraphQL spec does not define when a deprecated field or enum will be gone as versioning schemes like Semantic Versioning do. The spec only says that introspection tools, which are an integral part of GraphQL, should discourage developers from using deprecated fields.
Often, REST APIs don't use introspection tools for API discovery and documentation, but only supply developers with written documentation. In case of an evolving REST API, the deprecation warnings should go into these docs.
In both cases, it's essential to inform the developers of your clients about these changes.
How to version?
In case we decided to version our API, we now have to ask ourselves, how should we do it?
This question can be split up into two questions:
- Which versioning scheme should we use?
- How should we add versions to our API?
Versioning Scheme
Most APIs use a custom versioning scheme based on Semantic Versioning. The basic idea is, we have three numbers, z.y.x
and increment one of them by one on a change.
If a change
- is a bugfix, x is incremented.
- adds a feature, y is incremented.
- breaks backward compatibility, z is incremented.
If y is incremented, then x is reset to 0 and if z is incremented y and x are reset to 0.
There are nuances, but that's the gist of it.
Versioning Implementation
There are different ways to encode versioning into our API.
- Include the version into the URL
- Include the version into the request header
Each is having different pros and cons.
What is Moesif? Moesif is the most advanced API analytics service used by over 2000 organizations to measure adoption rates of new API versions and SDK releases by your customers.
Version in the URL
The upside of encoding the version into the URL is a better developer experience.
/api/v1.4/products/123
Many API client developers and often even their users are using a browser to check and debug APIs. Copying around API-URLs is a common practice, and encoding versions into a header would prevent this.
Roy Fielding wrote in his dissertation two things about this. First, one of the goals the REST architecture style has is stable URLs, and every client should be able to request the representation format it understands.
If we don't add versions to our URLs, they are more stable, so it would be more RESTful to keep them out.
Also, if someone copies a URL they used in their API client into a browser, the browser is a different client and should be able to request the representation it prefers. Maybe the API client could only parse JSON, but a browser can display a full-fledged HTML page.
So in the eyes of REST, a browser isn't the right tool to debug a REST API, a tool like Postman would be more suitable because it allows the developer to define headers that in turn define which representation will be requested.
However, we are free to do as we like and as GraphQL shows it isn't necessary to adhere to REST if you want to build a consistent API. If we think an unRESTful approach suits our browser-debugging-users better, we can go down that road.
Version in the Headers
The other approach is to put the version in the request headers.
The HTTP specification says:
The Accept request-header field can be used to specify certain media types which are acceptable for the response.
Fielding wrote that a representation has a data format known as media type and the HTTP specification defines an Accept
request-header to specify the accepted media type. So if we want to do it RESTful, we should put it right there and not into a customer header.
The definition for a vendor (API creator) specific media should look as follows:
type "/" "vnd." subtype ["+" suffix] *[";" parameter]
-
type
is one of the following:application
,audio
,example
,font
,image
,message
,model
,multipart
,text
,video
-
subtype
is a string likemycompany.api
-
suffix
is one of the following:xml
,json
,ber
,der
,fastinfoset
,wbxml
,zip
,gzipcbor
-
parameter
is a string likeversion=1.2.5
Some examples for encoding the version and format of the API into the media type:
application/vnd.mycompany.product+json;version=1.2.3
application/vnd.other-company.api-3.7+xml
Btw. the default Accept
header for a GET request of my browser looks like this:
text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
To give a taste of what media types a browser expects when it navigates to an API link.
Conclusion
Versioning or evolving an API and how to do so is by no means an easy topic.
Evolution and versioning are different paths we can follow, and if we go for a versioned approach, there is no one winner.
While the famous REST dissertation written in 2000 is rather old, it comes directly from a highly influential person in the field of Web technologies, on the other hand, GraphQL shows that unRESTful approaches to API design can be equally successful.
Whatever path we take, it's important to consistently follow it and keep our users up-to-date about what we're planning to do.
Moesif is the most advanced API Analytics platform, supporting REST, GraphQL and more. Over 2000 organizations use Moesif to track what their most loyal customers do with their APIs. Learn More
Originally published at www.moesif.com
Top comments (3)
I think the most important question to ask yourself is “how long will we support deprecated features?” And then make the answer clearly visible to consumers.
Ultimately, the biggest concern that might lead you to versioning is compatibility breaking changes (things that would increment a semver major number). There are ways aplenty to prevent new features from breaking compatibility, so really breakage will just happen when you remove stuff.
If the answer to “how long will we support this” is “in perpetuity” then yeah, you need to version. If the answer is anything else, I don’t think you need to version. If your contract with your clients is “we guarantee compatibility for x months”, then it’s up to the users to check for updates at least once within that time period.
There’s nothing stopping you from versioning even if you limit support. If you do it would let you keep your code clean and just leave older servers / containers running until their support window ends. Separate servers also means you can check which client api keys are still using old services and proactively send them notifications.
Inspiring talk by Rich Hickey about change, versioning and semantics youtube.com/watch?v=oyLBGkS5ICk
¡Great article Kayis and very useful! I invite you to review my latest post about the same topic: dev.to/jonathanbrizio/real-world-d...