How Hashnode scales custom domains feature

Introduction

Over the past year at Hashnode, we have been continuously developing GraphQL APIs to enable users to interact with their data and perform various operations. We utilize Stellate as the GQL CDN and take advantage of its Edge Caching capabilities.

Line added by Dev

Now that we are discussing Caching, it is important to emphasize a crucial aspect that follows: Cache invalidation.

Out of the methods provided by Stellate for cache invalidation, we utilize two of them:

  1. Manual invalidation- This is performed via the Purging API provided by Stellate. We have implemented it in an event-based mechanism where specific emitted events trigger the purging of cache for a particular entity, such as a User or a Post. Since this process occurs asynchronously, it is beneficial when immediate fresh data from the origin is not required.

  2. Mutation-based- This method allows for the synchronous purging of cache for an entity using a unique identifier, such as id. For example, if we need to change the title of an article, we would use the updatePost mutation as follows:

     mutation UpdatePost($input: UpdatePostInput!) {
       updatePost(input: $input) {
         post {
           id
           title
         }
       }
     }
    

    In the mutation response, when requesting post fields, we also retrieve the id field. This id enables Stellate to recognize that the next time the same entity is requested, the data should be fetched from the origin, and the cache should be updated with the new data.

The points mentioned here can possibly extend to any GraphQL CDN caching service you may use. Requesting an unique identifier like id in your Queries, Fragments, or Mutation responses becomes essential when there is a caching layer between your users and the server. Without the identifier, you might encounter stale results.

The problem

As we delved into using GraphQL queries and mutations to build features, we encountered numerous bugs. The primary cause was the absence of the id in the queries and mutation responses. This issue becomes evident when performing a mutation and expecting the subsequent query to return fresh results.

Reminding developers to always remember to retrieve the id field was not a sustainable solution, given our fast-paced development and how easy it is to overlook.

The solution

We needed a more reliable method to ensure developers include the id field before merging their code. The ideal place for this check is in our CI/CD pipeline. Our linting rules run within a GitHub action, and if these rules are not followed, the developer must correct the issue before they can proceed with merging their changes.

Implementing a linting rule to notify developers about missing id fields appeared to be the most effective solution. Consequently, we began researching existing rules and discovered the eslint-graphql plugin.

This plugin offers a wide array of rules that can be applied to lint both GraphQL schema and GraphQL operations. One rule that proved particularly useful to us was require-id-when-available. The name itself indicates its functionality. Although this rule is marked as deprecated in the documentation, its renamed version, require-selections, is currently available only in the pre-release version.

Configuring the plugin and the rule can be accomplished by adding it to the overrides list in your eslint configuration file (.eslintrc.json in our setup):

    {
      "files": ["*.graphql"],
      "parser": "@graphql-eslint/eslint-plugin",
      "plugins": ["@graphql-eslint"],
      "parserOptions": {
        "skipGraphQLConfig": true,
        "schema": "https://gql.hashnode.com",
        "operations": "**/*.graphql"
      },
      "rules": {
        "@graphql-eslint/require-id-when-available": "error",
      }
    }

By default, the plugin will search for a schema file, which can be a .graphql or other supported extensions (such as .json) that you may have configured. In our case, we had the schema file locally but did not push it to the remote repository (it was ignored using .gitignore). Therefore, in the parserOptions, we specified skipGraphQLConfig as true and set the schema field to our official GQL URL.

Not all rules in this plugin require the GQL schema, but require-id-when-available does need to know where to expect the id field in the GraphQL documents.

Here is how the plugin detects and reports the missing id:

Conclusion

When we configured the plugin, we discovered 9 more places where we missed retrieving the id field. This plugin is set to significantly reduce the time we spend on debugging caching-related issues, allowing us to dedicate more time to building exciting new features for our users.