Continuous Evolution & GraphQL: A Double-Edged Sword
When GraphQL was released a few years ago, every blog post, talk or announcement always included the fact that GraphQL prides itself on not using the typical API versioning to manage changes, and that in fact, the “GraphQL way” was more about Continuous Evolution. I find Phil Sturgeon describes this well:
API evolution is the concept of striving to maintain the “I” in API, the request/response body, query parameters, general functionality, etc., only breaking them when you absolutely, absolutely, have to.
This is quite different to versioning your API through the URL for example, where you would never make important changes to an API after the day it’s shipped. API evolution gives the API developer more flexibility and freedom, and allows clients to enjoy never having to migrate their app to a new completely different version (in theory).
Although the concept of API Evolution recently got a big popularity boost with the arrival of GraphQL, it is not new at all. API evolution has been a good way to manage change in our APIs for a while, and can be done as well in a REST API. The Sunset header is pretty promising in that regard.
However, GraphQL truly makes it easy(ier) for us to manage these API changes with its type system and schema. It even gives us a way to deprecate schema members, allowing us to sunset, or discourage usage of certain parts of our APIs.
Now, GraphQL has made it really easy for us, but have we taken this for granted?
The other edge of the sword
While continuous evolution helps us making changes without versioning our APIs, it should not be mistaken for a free pass for subpar API design or for “trying things out”. The move fast, break things motto has great uses, but its consequences are much harsher when it comes to maintaining an API. Breaking things in our APIs involves breaking other people’s integrations 👎
This is the part of the earlier quote I really want to stress:
only breaking them when you absolutely, absolutely, have to.
Although deprecations allow us to introduce breaking changes more carefully, in the end, they still result in a breaking change. They also result in additional work for our integrators / clients. It’s easy to mark something as deprecated, but it’s hard to deal with the deprecations on the client side of things. This is something we must understand before using deprecations as a crutch.
What Can We Do About It?
If we want to keep our continuous evolution privilege, we must take deprecations and breaking changes seriously. Here’s a few things you can do to keep that privilege, maintain the trust of your integrators, and make your life easier when it’s time to make API changes:
- Build an Evolvable Schema
The first thing we can do is to avoid breaking changes all together, or minimize the risks of them happening. There are ways to build a GraphQL schema in a way that changes might be less painful to make! I wrote a post on this exact subject a few days ago.
- Document And Communicate
As I outlined before, a deprecation always ends up with a breaking change. The advantage with using deprecations is that it allows us to set up that breaking change and make our integrators aware before executing the change. GraphQL’s type system allows us to encode this information right in the schema. With this metadata, we can automatically generate documentation. At GitHub, we’ve got a change log for clients to discover new schema members, the breaking changes page to see upcoming breaking changes, and schema previews, to announce and try out new parts of the schema. Each of those makes sure our integrators are aware of past and future changes.
While this helps, these are all “passive” ways to get information on upcoming changes. To make sure breaking changes go well, you might need to reach out to your integrators. This leads to the next tip.
- Keep an Eye on Schema Usage
If you have the tooling in place, try to log, or persist data on how your schema is used on a daily basis. This includes which fields are used in queries, arguments, input values, users using those, etc. Evolving your schemas becomes so much easier when you have that in place. Since GraphQL is so explicit, (it has no “select *”), we know every single thing that is used in our schema. This could let you safely remove members when they are not used anymore, reach out to users that are querying deprecated fields, and any other insights you could derive from that data.
- Provide alternatives
Without any alternatives to a deprecated schema member, how can we expect existing integrators to even stop using that field? They simply can’t. If you can, always provide an alternative such as another field, another way to do things, another API, etc. At GitHub, we try to always include the alternative in the deprecation message.
Build new features into that alternative fields and not in the deprecated paths of your schema. This way, if integrators needs access to the shiny new feature, they have an incentive to stop using the deprecated stuff!
Next time you add a feature to your API, remember that deprecations are there when absolutely needed, make sure you’ve thought hard about your schema before shipping 🧠. These tips should should make it a bit easier to deal with breaking changes when they must happen.
Remember! With great power comes great responsibility! 🕷