Requirements change, but API contracts are forever - I wish! A breaking change is something that is not backwards compatible. This means that a consumer of your API would not be able to use the new version without making a code change themselves. We avoid breaking changes if possible, but there are cases when they are inevitable. It could be something small: like making a mandatory field optional. Or it could be something big: like removing a query or a mutation. In this talk we'll review the types of breaking changes you may encounter and how to deal with them gracefully.
Handling Breaking Changes in GraphQL
AI Generated Video Summary
This Talk discusses handling breaking changes in a GraphQL schema, including the use of the deprecated directive to tag fields that should no longer be used. It also covers the process of deploying GraphQL APIs and mobile apps, highlighting the challenges of mobile app release adoption. The Talk emphasizes the importance of making safe upgrades in mobile apps and provides strategies for detecting and handling breaking changes, such as using TypeScript and GraphQL Inspector. Overall, the Talk emphasizes the need to minimize user impact when introducing breaking changes in GraphQL schemas.
1. Introduction to Breaking Changes in GraphQL Schema
Hello everyone, in this part, I will talk about handling breaking changes in a GraphQL schema. I will explain what a breaking change is and provide a code example to illustrate it. Let's get started!
Hello everyone, it is a pleasure to be here and it is a pleasure to talk about something that is very relevant to my work, which is handling breaking changes in a GraphQL schema.
So, just a quick intro about me. My name is Kadi Kraman, I'm currently the Director of Mobile Development at Formidable. If you've not heard of us, Formidable is a consultancy. We build websites, mobile apps, and of course, GraphQL APIs. In my five years in this company, I think almost every single project had a GraphQL API in it. So we've been doing this quite a bit. Back in 2002, my first GraphQL API was around 2017, and since then, I've worked with both small and large applications, and mostly as an API consumer.
So, because I build a lot of mobile applications, usually I've been the client of a GraphQL API. I would spend maybe 20 per cent of my time writing GraphQL, and 80 per cent of my time using GraphQL APIs. So, breaking changes is something that's very relevant to me because I'm the client that gets affected if breaking changes happen.
So, what is a breaking change? A breaking change in a GraphQL schema is basically an update that causes the API contract to change in a way that is not backwards compatible. Now, what does this actually mean? I'm going to show you using a code example. So, here we have a little schema. We have a query where you can query a user by an ID and it will return you a user type. And the user type has just the ID and the name in it. Both of them are mandatory fields. And if we look at what a query would look like. So, here on the right, we're going to query the user by ID, query the ID and the name, and we get exactly that data back. So, say that you had this user type in your current project and the client comes back to you and says, hey, we don't want to have our name as a single string. We want to have the first name and the last name separately. So, as a new requirement, you need to separate the name field into first name and last name. So, how would you go about it? You would be tempted to do something like this. So, here we've added the first name and last name which are both mandatory strings. But then, because we really don't use the name anymore, we can remove the name field. Now, this seems all well and good from the schema side, from the API side. When we look at the client side and query the same query that we did before, we are actually met with an error. And the error says cannot query field name on type user. And indeed, the name field no longer exists. Now, this is an example of a breaking change.
2. Handling Breaking Changes in GraphQL Schema
A change that is not backwards compatible. Instead of deleting the field, we can add a deprecation notice using the deprecated directive. The deprecated directive allows you to tag a field in a schema as a deprecated field to communicate to your consumers that this field should no longer be used. It is part of the GraphQL spec and most GraphQL servers should be implementing it. Insomnia, a GraphQL client, provides an example of how deprecated fields are displayed in the schema. Now I will show you a way to introduce a breaking API change in a GraphQL API relatively safely by following the steps: add, deprecate, migrate, remove.
A change that is not backwards compatible. Because a query that worked on the previous version of this API no longer works because of this change. Let's look at how we would make this change backwards compatible. It's actually surprisingly simple. Instead of deleting the field, we can add a deprecation notice using the deprecated directive. Here you've seen next to the name field, I've added deprecated and also provided a reason. So in this case, it's deprecated in favor of first name and last name.
And here we can see that the previous query that queried just the ID and name still works and the new query that queries the first name and last name still works. Therefore, because both the previous and new queries work, this is a change that is backwards is compatible. The deprecated directive is really handy. It's basically it allows you to tag a field in a schema as a deprecated field to communicate to your consumers that this field should no longer be used. It is part of the GraphQL spec, which means that most GraphQL servers should be implementing it. The most important part, most IEDs, GraphQL tools, and clients that you might be using will pick up this notification and warn you if you're using a deprecated field.
Here I've added an example. This is from Insomnia, which is a GraphQL client that I use for querying. When I look at the schema and the user type in particular, we can see that the name now has an exclamation mark next to it. If I hover over it, it's telling me that the field name is now deprecated, and it's also giving me the deprecation message that I wrote. So it's deprecated in favor of first name and last name. Now there might be a reason, however, rarely, that you'd simply have to introduce a breaking API change. So I'm going to show you a way that you could introduce a breaking API change in a GraphQL API relatively safely. The key words you need to remember are add, deprecate, migrate, remove. Unfortunately, not a very good acronym. Let's go through these one by one to show you what each step means. So this is our starting state. We're going to start with this user type that we used before, just with the id and a name. And this is our application stack. So we're going to have a website, let's say that's an Xjs app. We're going to have an API. It's of course a GraphQL API. And because I build mobile apps, we're going to have a React Native app for iOS and Android in a third repository.
3. Deploying GraphQL API and Mobile Apps
All three repositories are deployed separately. The first version of web1, api1, and app1 implement the user type. The stages include adding schema fields, deprecating fields, releasing a new version of the API, migrating clients, and removing deprecated fields. Deploying mobile apps is more complex, with different versions being released to stores and a gradual transition of users.
So all three of these are in separate repositories and they are deployed separately. And the first version of these web1, api1, and app1 all implement the user type as seen here.
Now the first stage add is pretty straightforward. We're going to add the schema fields that need to be added. In this case, we're going to add the first name and last name to the user type.
The next stage deprecate, you would usually do it together with add. But in this one, we're going to deprecate any fields we might want to remove in the future. Here we've added the deprecated directive. We use the deprecated directive to add a deprecation notice to the name field in the user type.
Now when we're finished with all our changes, we will release a new version of the GraphQL API to api2. Now, api2 is still compatible with app1 and web1 which means this is a backwards compatible change.
The next step is the migration step. Now in this step, you have to go through all the clients, everything that's using your GraphQL API, and update them to use the new API. Then we'll go to the Website and the app. We'll do the change and we'll publish the new versions and ensure all of our users are using them.
Now when you're confident that no-one is using web1 and app1 any more, and you've published web2 and app2, then it might be safe to remove the field. So here we can remove the name field that was deprecated from the user type and when we're finished we will deploy the new version of the API to api3. Now api3 is not compatible with web1 and app1, they would still get an error, but if no-one is using them, you've already published the change, they will be using web2 and app2, then that is a fine deletion to make.
Now because I build mobile apps, I wouldn't be a mobile app developer if at this point I didn't go on a tangent on why this is so much harder on mobile applications. On a higher level, this is what deploying websites and APIs looks like. You've finished your version 1, you're going to upload it to wherever you release it, say AWS, it will become available to users, and 100% of your users are going to use it.
Now when it comes to releasing v2, when you finish coding it, you're going to upload it to AWS, you're going to make it available and instantly all of your users are going to switch over and 100% of your users will be using v2 and no one will be using v1. When you finish v3, you'll release it, 100% of your users will immediately be switching over.
Now this seems pretty obvious but the fact is, this isn't at all how it works for mobile applications. This is what deploying a mobile app looks like. When we've finished our v1, we're going to upload it to the stores, we're going to get an approval, and when we've got the approval we can publish it to the app and play store. And because it's the first ever version of our app, 100% of our users are going to be using it.
Next, when we're ready for v2, we're going to release it to our stores, we're going to get it approved, we're going to release it to our stores. We're going to, it actually takes time so we're going to go away for two weeks, we're going to look at the stats after two weeks, and we'll see that maybe 60% of the users will use it, will be using it. And 40% of users are still using v1.
4. Mobile App Release Adoption
Mobile app releases never get 100% adoption like websites do. Apps only get updated if the user has automatic updates turned on or if they manually update from the store listing page. There can be a significant delay between publishing the app and users actually getting the update.
And when we do another version, v3, we get it approved, we release it to the stores, we go away and come back after two weeks, we'll find that maybe 65% of the users are using it. 30% of users are still on v2 and 5% of the users are still on v1. Now the sad truth is that mobile app releases never get 100% adoption like websites do. And the reason for that is apps only get updated if the user either has automatic updates turned on, or if they go to the store listing page and update manually. Even if the user has automatic updates turned on, the update wouldn't be instant, as soon as the app is available, it will get updated. Instead, usually it will get updated once you plug in your phone and you connect it to the Wi-Fi. So there might be a huge delay between when you publish the app, and the users, even those who are first in line, actually get it.
5. Making Safe Upgrades in Mobile Apps
It's never safe to make a breaking change in a mobile app, but there are ways to make the upgrade as safe as possible. Building in a fail-safe prompt for users to upgrade is a last resort. Monitoring query usage and active users can help assess the impact of the breaking change. Break-in API changes are more significant for mobile apps than websites. Changing a mandatory field to optional in a GraphQL schema is a breaking change. This can be seen in a code example using TypeScript.
Knowing that, you might be asking, when is it safe to make a breaking API change, if it's used by a mobile application? Well, the truth that you don't want to hear, is that it's actually never safe to make such a breaking change. Because you will always affect someone, it is impossible to get 100% of your users, once your latest version.
But, what if I really really really have to? Well, there are ways to make this upgrade as safe as possible. Usually in mobile apps, we build in, so this is something you would have to build in as a developer, we would build in a fail-safe, which basically is a prompt that would prompt users to upgrade, if they want to continue using the app. Generally, most apps have it built in, but it is a last resort, we don't want to use it, it should never be part of your normal development flow because it's terrible UX and users hate it, and to be honest, Apple also hates it, and we don't want to piss off Apple.
As I was actually working on this talk, one of the apps that I use regularly had this app upgrade prompt, so I've added it here as an example. Basically, when I open the app, I will immediately get a native alert that says, this version is deprecated, please go to the store to upgrade it. So if I press on learn more, it will take me to the app store, where it will prompt me to upgrade. I've actually seen they did a complete revamp of the whole application. I think they rewrote it, so presumably, the previous version is no longer compatible. Now, even when you get to this point, and you've prevented users from using the old version of your app, you technically can't even then be sure that they're going to upgrade. Because at this point, I can choose to not press the update button on the app store because maybe I'm not on Wi-Fi and I don't want to use my mobile data. And also I can just decide that this annoys me and I'm going to use this app on the website instead, or not use it at all. And once you've done everything you can to make sure that users will update, then you just need to monitor that they are doing so. You'll do this in two levels. One is on the API side and one is on the app side. So on the API side, this will be maybe an AWS or in CoreLogix. You can generate metrics to monitor query usage. So you can generate a metric to monitor the usage of deprecated fields and you'll see whether or not they get called or not. And then on the other side, you can also monitor your apps are active users for each app version to kind of get an idea of the number of users still using app versions that are deprecated and that have this break-in change and how many users this break-in change could affect. And one thing to note is that having break-in API changes generally is much more significant for mobile app users and developers than it is for web users and developers. Because on the website, if your website goes down or it doesn't work, then usually your users could tweet at you, they could maybe email your customer service angrily, whereas if an app is broken, if the API doesn't work, then users have a very nice and convenient way to complain about it and they will do it in a store listing page and give you a one-star rating because of a single-line change where your API didn't work. Going away from that dreary topic, let's play a little game. Which one of these is a breaking change? So we've got the user type with the ID and a name, and on one side I've made the name from mandatory to optional, and on the other side I've made it from optional to mandatory. Did you see the first one? Indeed, in a GraphQL schema if you have a field that is mandatory and then you change it to be optional, that is a breaking change. And why is that? Well, it might not be very easy to see it from the schema, but it will be a little bit simpler to see as a code example. So here is, I've used TypeScript just to make it a little bit more obvious. And on the left, we have some code that you might have on the front end that uses this API. So because our user only has an id and a name, I might have a utility function that uses the name and takes the first parts of it as the first name. Now after I've done the change, so I made the name from mandatory to optional.
6. Detecting and Handling Breaking Changes
Let's do one more. In this case, I've got the user query and I'm changing the user ID that I'm querying it by from mandatory to optional or optional to mandatory. Did you see the second one? Yes, the second change is breaking. And it can be confusing because it's the opposite for query inputs because here, changing the user ID from optional to mandatory is breaking, not the other way around. I've used the diffs here on purpose so you can see that as a reviewer it is actually really easy to miss these kinds of changes.
Thankfully, there is a tool to help you. If you haven't checked it out yet, I would definitely recommend looking at GraphQL Inspector. We're currently using it on my project and it's made a huge difference. It does a bunch of things, but the most important thing for us is that it allows us to have a check on pull requests, which will tell us if a breaking API change will be introduced. You can use this on any kind of CI. We use GitHub Actions, so I'm going to show that as an example. Here what we're configuring is that it's going to compare the GraphQL schema in the current to the GraphQL schema in your current working branch. This is what it looks like on the actual pull request. On the left, we're going to see an example of a breaking change, so we have it set to fail CI, if there's a breaking change. And you can see the two types are being removed. Removing types is always a breaking change, and therefore this gets highlighted. And the other cool thing is actually on the right, we can see some examples of just kind of information notices. So these are not errors or warnings, it's just for information. And what the GraphQL inspector does is that it highlights any changes, so this will draw the attention of the reviewer to what's actually changed. And this is really important because you have to be quite aware of what you're actually adding to your GraphQL API, because as you can see on the other side, removing anything is always a breaking change. So once you add it, removing it could be a FAF. Because we've enabled fail on breaking, there's another thing that we've added, which is to have an approved label. So you can set your own label for an approved breaking change, so you can call us whatever we want, but this is us. So basically, if you add this label to a pull request, then even if it had a breaking change, it will allow the CI to pass. And this is really useful because basically, as a pull request gets introduced, a reviewer will go through it and they get highlighted that there is a breaking change, and a conversation needs to happen whether this is a change that we're happy to have in, is there another way to do it, and should be avoided altogether.
In summary, we looked at what is a breaking schema change. So this is renaming a field, deleting a field, changing the data type, making a required field optional, or making an optional input required. In general, you should avoid making breaking changes as much as possible, especially if your API is being used by a mobile application, or any kind of desktop application that doesn't have the same kind of publish workflow as websites and APIs. But if you do need to introduce a breaking change, you can use the add, deprecate, migrate, remove step to minimize the amount of users you're going to affect. And finally, if you haven't yet, I would very much recommend you check out GraphQL Inspector to improve your GraphQL API workflows.