Managing Breaking Changes in Hashnode GQL Public API

Managing Breaking Changes in Hashnode GQL Public API

ยท

5 min read

Hey everyone, this article discusses how we manage breaking changes in Hashnode's GQL Public API. We recently announced Public APIs 2.0 and have been rapidly iterating. As a result, we are incorporating feedback and enhancing the APIs. We are thrilled to see community members contributing insightful ideas on how to use the APIs.
This makes it highly crucial to manage every breaking change gracefully, allowing developers to transition smoothly without breaking their apps.

Before we dive deeper, let us first understand what we mean by a breaking change.

What is a Breaking Change?

There can be multiple categories of changes when working with GQL APIs.

Breaking Changes:

  • Changing the name of an existing type, field or enum value.

  • Removing an existing type or field.

  • Changing the arguments of a field (name, type, default value, etc).

  • Changing the type of a field (from String to Int, for example).

  • Adding pagination to a query.

Non-Breaking Changes:

  • Adding a new type, field or enum value.

  • Adding a new optional argument to an existing field.

  • Adding validation to an existing field.

One of the most common scenarios we encounter is changing the type or argument of a field. The API will most likely break if the change is not compatible.
Now since we have identified the breaking changes, we can go ahead and think about how to handle them.

How Do We Manage Breaking Changes

We follow a series of steps that involve multiple deployments in order to not affect the user.

Since we cannot guarantee that GraphQL and the front end will be deployed simultaneously, users might have a page open running an older version of JavaScript. If we deploy a breaking change during this period, the site will break. The requests will only be valid when the user reloads the page.

Let's take an example where we have to change the type of bio of a hashnode user from String to an Object containing html, text and markdown properties. The change is as follows:

""" BEFORE """
query Me {
  me {
    bio
  }
}

""" AFTER """
query Me {
  me {
    bio {
      html
      text
      markdown
    }
  }
}

These types are not compatible hence this will be considered as a breaking change. The steps to handle this breaking change will be as follows:

Step 1: Deprecation and Versioning Strategy

We will leave the existing bio field as it is and simply mark it as deprecated. This ensures that API users are informed about the deprecation early, and we avoid breaking any existing apps.

After that, we can add a new field and name it bioV2. This should be our new field that incorporates new changes.

bioV2 is supposed to be a stepping stone while we transition the field.

interface User {
    bio: String @deprecated(reason: "Use bioV2 instead")
    bioV2: Content
}

type Content = {
    html: String
    markdown: String
    text: String
}

Step 2: Providing a Transition Period and Backward Compatibility

We have added a new field, and the next step would be to remove the deprecated field at some point. However, we need to provide some time for API users, including Hashnode's own frontend, so that they can transition to the new API.

Updating the web frontend is easier since deployments are instant, and we only need enough time for users to refresh the page at least once. Typically, 1-2 days are sufficient for this. However, our mobile app also uses the GQL APIs, and sending updates to the mobile app is much more challenging. Mobile users don't update the app often, and there can be up to 10 older versions for which we have to consider backward compatibility before removing any deprecated field.

We have found that this period can range anywhere from 2 to 4 weeks and we usually decide to go with 4 weeks. This is also enough time to communicate with API users and allow them to transition to the new field.

We will have to provide a deprecation notice that can convey this information. The deprecation notice should include two things

  • Date when it will be gone

  • Alternative if available

Updated comment on @deprecated directive can be:

interface User {
    bio: String @deprecated(reason: "Will be removed on 10/10/2023. Use bioV2 instead")
    bioV2: Content
}

Step 3: Updating API Documentation

Since we added a deprecation notion using a directive, it will be updated on API Docs as soon as the change is deployed. This is important because we don't want anyone to refer to docs and still use the fields that are supposed to be removed/changed soon.

Step 4: Communicating Deprecation to Users

We have already added a deprecation notice and it is available on API docs as well. This is good but not enough since not everyone visits the docs often.

We have also created a communication channel on our discord server where we share information about every breaking change.

Step 5:Bring Newer Changes Back to the Original Field

Once the transition period has passed, we can transfer the changes from the V2 version field back to the original field and deprecate it. The V2 version field was only intended as a stepping stone while we transitioned the original field to the updated definition.

interface User {
    bio: Content 
    bioV2: Content @deprecated(reason: "Will be removed on 10/11/2023. Use bio instead")
}

type Content = {
    html: String
    markdown: String
    text: String
}

We will also update the front end, as we did previously, and switch from using bioV2 back to bio.

Once the transition period is over, we can remove the bioV2 field.

This was a relatively simple example, but sometimes changes are not so straightforward. For instance, adding pagination to an existing query can be more complex. In such cases, it would be appropriate to provide a migration guide.

Conclusion

We discussed how Hashnode manages breaking changes in its GraphQL Public API, ensuring a smooth transition for developers.

We covered the types of breaking and non-breaking changes, and outlined a step-by-step process to handle them:

  1. Deprecation and Versioning Strategy.

  2. Providing a Transition Period and Backward compatibility.

  3. Updating API documentation.

  4. Communicating Deprecation to Users.

  5. Bringing Newer Changes Back to the Original Field.

This approach helps maintain compatibility with older versions while incorporating new features and improvements.

Did you find this article helpful? Let us know in the comment section ๐Ÿ’ฌ.

References