Never worry again about your GraphQL API breaking changes
Problem
We deploy our API and our frontend at the same time. It means our new frontend contains the necessary changes to match the new API ⇒ great ✅.
⚠️ But a user can keep their browser opened with the previous frontend code version, as a consequence the previous frontend version calls the new API, and if there are breaking changes in the modifications ⇒ it breaks 💥.
Our previous solution
⚠️ Spoiler: not the one we recommend now, scroll down to ‘Our new solution’ if you want to go straight to the point
For a long time, here is how we have handled our breaking changes:
- deprecate the concerned field in the API instead of deleting/modifying it
- add the new one
- merge the pull request
- wait for 7 days: totally arbitrary timing, we considered that after 1 week, all our users would have refreshed their frontend
- remove the deprecated field
Pros and cons:
- 🟢 no impact at all for most users
- 🔴 not efficient for developers (2 pull requests, with a week delay between it)
- 🔴 not perfect: the 7 days delay is arbitrary, some users can still face a bug
- 🔴 we often forgot to clean the deprecated field: legacy code remaining in the codebase
We wanted to improve that situation.
Here are the solutions we considered:
- ❌ API versioning: too costly to put in place versus the gain + GraphQL does not recommend it (see best practices)
- ❌ Fields versioning: forces you to maintain legacy code in the backend
- ✅ Automatically refresh the user frontend when they face an API breaking change: this is the solution we chose and I will explain below how we implemented it.
Our new solution
👉 Automatically refresh the user frontend when they face an API breaking change
How to
- In your ApolloClient, if you do not have one already, you will need to add an errorLink
- Build this errorLink thanks to the given onError link furnished by @apollo in @apollo/client/link/error:
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, operation }) => {
if (graphQLErrors) {
// We are targeting here GraphQL API breaking changes due to schema changes
// which correspond to code 'GRAPHQL_VALIDATION_FAILED'
const validationErrors = graphQLErrors.filter(
({ extensions: { code } }) => code === 'GRAPHQL_VALIDATION_FAILED',
);
if (validationErrors.length > 0) {
const isMutation = operation.query.definitions
.filter(
(def: DefinitionNode): def is OperationDefinitionNode =>
def.kind === 'OperationDefinition',
)
.some(
({ operation }) => operation === 'mutation',
);
if (isMutation) {
/* In case of a mutation being affected by the breaking change,
* as the data they entered will not be saved
* we decided to display a pop up to the user
* so they can collect their data before losing it
* to avoid frustration, if it was a long text for example.
*/
if (
!window.confirm(
"Sorry, the page needs to refresh to work properly.\n\n** You'll need to enter your latest data again **\nNote that you can:\n1. Click on 'Cancel'\n2. Copy your data\n3. Refresh the page\n4. Paste your data back\n\nSorry again for the inconvenience.\nThis will lead to improvements for you down the road.",
)
) {
return;
}
}
window.location.reload();
}
}
});
Pros and cons:
- 🟢 no mental load for the developers (API fields can be modified/deleted with peace of mind)
- 🟢 the legacy code is removed right away
- 🟠 small impact on user experience: users can face an auto reload or a pop up in the case of a mutation
One last thing
Do not forget to stop your error alerting on ‘Validation errors’ if you had some, because you won’t need it anymore.
If you have any feedback, please feel free to comment! Thanks 🙏