What’s in Our GraphQL Context

Kenny Mcgarvey
Engineering @Varo
Published in
5 min readJan 31, 2022

For the last few years, I have been working with GraphQL, and during that time, I haven’t seen too much content regarding the context. Having used RESTful and RPC frameworks in the past, which have their own concept of context, I had a pretty good idea of how to use it effectively. However, in a RESTful API, the endpoint is only called once per HTTP request, and in GraphQL, any number of resolvers could be invoked per query. This means leveraging the context as a source of input can have a more meaningful impact on your development. I’m going to review some of the guidelines and ideas that we follow that I wish I had read before I started building GraphQL services.

Rules & Guidelines

The first rule, we don’t want our resolvers to know about our HTTP server. We should be able to swap out our server implementation (Express, Hapi, Fastify, etc.) with another without touching our resolvers. Any data we want to get from the HTTP request needs to be parsed within the context creation function. From there, we sanitize the input and make it available as strongly typed fields in the context.

Once the context object is returned from the context factory, it cannot be mutated. As far as our resolvers are concerned, the context is read-only, so no sneaking data between functions via the context object. This is to ensure that regardless of the path taken to get to the resolver function, the input and output remain consistent.

Last rule, we design our resolvers with test-driven development (TDD) in mind. Anything we may want to mock in a unit or integration test, we make available in the context of our GraphQL resolver functions. This, plus the first two rules, means our test code is very straightforward and easy to understand.

Now that we have covered some of the rules that guide how we use our GraphQL context, let’s look at some of the content.

External Data Sources

GraphQL is all about aggregating data, so naturally, we have our external datasources available to us via the context. This isn’t news for anyone who is familiar with developing GraphQL services. However, we differ from other examples on the web because we bind our authorization to the data source when we create them. In other words, our resolvers don’t need to think about what the current user can and can’t do. The important thing to note here is that these datasources must be lightweight wrappers since they will be created for every context (on each request).

A post about GraphQL datasources would not be complete without mentioning DataLoaders.They’re great — use them.

Translate

We try to minimize business logic on the frontend and keep it in GraphQL, so inevitably copy/text needs to live on the server. To stay organized, all of the copy is defined in a JSON file at the service layer, 1 file per language. During context creation, we bind the locale and language to our translate function that we provide to the resolvers. Anytime we need to return a string of text, we can use something like context.translate(‘customer_greeting’,{ name: ‘world’ }). The translate function is also able to format various data types like numbers, money, and dates. This makes our resolvers very simple when it comes to generating long pieces of copy with dynamic fields. For testing, we can mock the translate function to validate it was invoked with the correct arguments instead of checking the output string of the resolver.

Transform

Similar to translating various types to strings, we often need to convert between types. Numericals, Money, and the big one — Dates. Each of these could justify us making a suite of helper or utility functions to deal with them. As is the theme of this post, we put them in our context object. The only real benefit I can think of is it is really easy to see the entire library of transform functions available and where they are being used.

Feature Flags

In a previous post, I walked through feature flags in GraphQL and Apollo Federation. It should be pretty obvious how we leverage them within our GraphQL code via the context. All of our feature flags are in a single object on the context, strongly typed, and easy to use in the resolver. If we need to check if a particular feature is enabled, context.features.productA. If we need to write a test that tests this GraphQL query, we can set that field to true or false, depending on the use case.

Logger and other Metadata

There is some valuable information on the HTTP headers, but we can’t put it as is in the context object following our own rules. That said, the only time we’ve needed it is for logging errors, so we just put the data into our logging framework. Most loggers support per-request clones, so we make a child logger as part of our context creation. Additionally, we add request-id and operation name as metadata to the logger. When our GraphQL resolvers log errors, it automatically includes a splat of relevant information about the request to help us trace any issues.

Bonus: now

To be completely honest, I am more proud of this idea than I should be. We have a field, context.now, which is a simple Date object created at the start of the GraphQL request. Since our resolvers never do new Date(), it is effortless for us to control the “current time” in our tests. Funny enough, this is just a significant side effect of the main reason we did this.

Our GraphQL environment is federated, so we have multiple GraphQL graphs behind a single gateway. The now variable is created at the gateway layer and then shared with the services on every request. Not only can we guarantee that the time (“now”) remains consistent across resolvers, but also consistent across services (for a given GraphQL query). There are not many cases where this would cause any problems, but this is low-hanging fruit to skip time-related bugs entirely.

Most of these ideas just stem from “how can we test our resolvers” and “how can we keep them simple.” None of this is too profound or groundbreaking, but it would have been very helpful or at least validating if I had read these three years ago. Some of my reluctance was due to any performance hit or a bloated context, but so far, we have noticed no added latency or development overhead. Hopefully, at least one reader will be encouraged to see what functionality they could move to their GraphQL context.

--

--