Frankly, it is not very difficult to create a service interface -- a Web API. Proof of this fact is the long list of tools that read database schema and/or scan existing codebases and output HTTP APIs, ready to deploy into production. The actual production of the interface is easy. But the work of designing a well-crafted, long-lasting API is not so easy. And a key element in all this is time. Services that live a long time experience updates -- modifications -- that threaten the simplicity and reliability of the API that connects that service to everything else. Time is at the heart of the modifiability problem.
The good news is that, as long as your service never changes, your API design and implementation needs can be minimal. This is true, for example, with APIs that expose mainframe or minicomputer based services that have been running for decades. It is unlikely they will change and therefore it is pretty easy to create a stable interface in front of those services. That's were schema-based and object-based API tooling shines.
But there are fewer and fewer of these kinds of long-running services in the world today. More often APIs need to connect to services that were recently created -- those services that are just a few years old. And many times these services were designed to meet immediate needs and, as time goes on, those needs evolve. The easy route is to create a "new" interface, slap an identifier on it (e.g. "/v2/") and republish. There are many companies that do that today.
The good ones keep the old versions around for a long time, too. That makes it possible for client applications to manage their own upgrade path on their own timeline. But many companies retire the old version too soon. That puts pressure on API consumers to update their own code more quickly in order to keep up with the pace of change on the service end. When you consider that some API consumer applications are consuming more than one API, each on their own update schedule, it is possible that an API consumer app is always dealing with some dependency update challenge. That means API consumers are in a constant state of disruption.
A better approach is to adopt a pattern that allows services to evolve without causing client applications that use them to experience breakages or disruption. What it needed is an API design approach that supports both service evolvability and interface stability. And a set of principles that can lead us to stable, evolvable interfaces are 1) adopting the "Hippocratic Oath of APIs", 2) committing to the "Don't Change it, Add it" rule, and 3) taking the "APIs are Forever" point of view.
The Hippocratic Oath of APIs
One way to address the modifiability problem is to pledge to never "break" the interface -- the promise to maintain compatibility as the interface changes. This is a kind of Hippocratic Oath of APIs. Written between the fifth and third centuries BCE, the oath is an ethical promise Greek physicians were expected to follow. The most well-known portion of the oath is the line "I will abstain from all intentional wrong-doing and harm". This is often rephrased to "First, do no harm." And this line is an excellent guide to maintaining service APIs that evolve over time.
When creating interfaces, it is important to commit -- from the start -- to "do no harm". That means the only kinds of modifications you can make are ones that don't break promises. Here are three simple rules you can use to keep this "no breaking changes promise":
1 - Take nothing away
Once you document and publish an endpoint URL, support for a protocol or media type, a response message, an input parameters or output value, you cannot remove them in a subsequent interface update. You may be able to set the value to +null+ or ignore the value in future releases, but you cannot take it away.
2 - Don't redefine things
You can change the meaning or use of an existing element of the interface. For example, if the response message was published as a user object, you can't change it to a user collection. If the output property count was documented as containing the number of records in the collection, you cannot change it's meaning to the number of records on a single page response.
3 - Make additions optional
After the initial publication of the API, any additional inputs or outputs must be documented as optional -- you cannot add new *required* properties for existing interfaces. That means, for example, that you cannot add a new input property (backup_email_address) to the interface action that creates a new user record.
Don't Change it, Add It.
By following these three rules, you can avoid the most common breaking changes for APIs. However, there are a few other details. First, you can always create new endpoints (URLs) where you can set the rules anew on what inputs and outputs are expected. The ability to just "add more options" is a valuable way to support service evolvability without changes existing elements.
For example, you may have the opportunity to create a new, improve way to return filtered content. The initial API call did not support the ability to modify the number records returned in a response; it was always fixed at a max of 100. But the service now supports changing the page_size of a response. That means you can offer a two interface options that might look like this in the SIREN media type:
"actions": [
{
"name": "filter-list",
"title": "Filter User List",
"method": "GET",
"href": "http://api.example.org/users/filter"
"type": "application/x-www-form-urlencoded",
"fields": [
{ "name": "region", "type": "text", "value": ""},
{ "name": "last-name", "type": "text", "value": "" }
]
},
{
"name": "paged-filter-list",
"title": "Filter User List",
"method": "GET",
"href": "http://api.example.org/users/paged-filter"
"type": "application/x-www-form-urlencoded",
"fields": [
{ "name": "page-size", "type": "number", "value": "100"},
{ "name": "region", "type": "text", "value": ""},
{ "name": "last-name", "type": "text", "value": "" }
]
}
]
Note that you might think, instead of offering two forms, it would be safe to offer a single, updated form that includes the page_size property with the value set to "100". This is not a good idea. An existing client application might have been coded to depend upon the return list containing more than 100 elements. By changing the default behavior to now return only 100 rows, you might be "breaking" an existing client application.
Adding new elements is almost always safer than changing existing elements.
APIs are Forever
The interface -- the service API -- is the contract service producers make with API consumers. Like most contracts, they are meant to be kept. Breaking the contract is a seen as breaking a promise. For this reason, it is wise for service API designers to treat the API as "unbreakable" and to assume they will last "forever". Werner Vogels, Amazon's CTO puts it like this: "We knew that designing APIs was a very important task as we’d only have one chance to get it right." While this seems an daunting task, it can be made much easier when you build into the API design the ability to safely change elements over time.
Of course, things change over time. Services evolve. They add features, modify inputs and update outputs. That means the contract we make with API consumers needs to reflect the possibility of future changes. Essentially change needs to we "written into the agreement."
While it is not reasonable for interface designers to be able to accurately predict what the actual interface changes will be ("In two years, we'll add a third output property called `better-widget`"), it is reasonable for designers to create an interface that takes into account the possibility of future changes ("API consumers SHOULD ignore any additional properties that they do not understand."). In other words, the API design needs to not only accurately describe the current service interface, it needs to account for possible future changes in a way that helps API consumers to "survive" any unexpected evolution of the interface.
Lucky for us, there is a well-established approach we can use to account for variability in the interface over time. And that method is called hypermedia.