Kapost Engineering

Recent Posts


Recent Comments


Archives


Categories


Meta


Implementing an API Gateway with GraphQL: Practical Implementation Advice

Nathanael BeisiegelNathanael Beisiegel

We are happy to share that we have open-sourced an example implementation of a GraphQL API Gateway on GitHub. This repository includes some real-world concepts, including HTTP caching, error formatting, model organization, defending against bad data, mocking data, and tracking over statsd/datadog. We hope this code can serve as a reference to you to implement some of these features. The README includes a lot of educational and implementation information and we hope you will check it out as a reference!

If you missed it, see my previous post on resolution strategies in a GraphQL API Gateway! There we discuss the merits and resolution planning necessary to implement joins over REST requests. As I discussed in that post, an API Gateway can be a good way to address network complexity problems when you have a variety of microservices with APIs you need to access from the client. Apollo’s Principled GraphQL guide recommends this when you already have a set of services and are working to join it into one graph.

Once you’ve had a chance to go over the project, I’d like to highlight some of the concepts here that we think are generally useful to all GraphQL API Gateways, even if you opt to implement them differently. I hope some of the observations here are useful to you as you explore the source.

Connectors and Models

Credit: Image taken from the design docs from Apollo’s graphql-tools

These concepts are introduced by Apollo GraphQL here. Connectors are objects that are responsible for directly interfacing with data stores, such as Postgres or another database. In our API Gateway example, we have a single HTTPConnector that implements the core interface with other APIs via Axios. It includes a Redis cache to cache responses to allow for proxied 304s across the backends. This means we can avoid costly database operations when they are frequent. Even better: for responses that do not change across users, we can share the cache across all users!

Models are the glue between connectors and resolvers. They provide high-level methods for fetching specific resources. Similar to how you might treat models in Rails—you can define methods to fetch and return data. Models should typically implement mechanisms for batching and memoizing expensive calls so that resolvers can naïvely call them. For example, we might memoize the fetches for getting playlist, so we can skip fetching playlists we may have already grabbed. Models only memoize and batch per request (created in buildContext) so they are a great way to reduce redundant fetches down your resolver tree generically.

Response classes and Optionals

Now models could return raw JSON directly to resolvers. We have found that can lead to repetitive, defensive access that is better solved with an abstraction. To avoid this raw access in each resolver, we recommend wrapping each API up in a Response class. We provide an abstract base Resolver class for this purpose in our API Gateway. Each different API shape (usually just a few per service) should have a concrete child class (like we do for service1 and service2) to wrap so resolvers can expect to access in a consistent manner.

This provides another great opportunity to generically memoize, so that common operations within responses (finding a specific object, camel casing, accessing pagination) are all neatly implemented efficiently.

There are also cases where resources are requested but do not exist anymore. For example, fetching a list of songs by ID may yield a missing or invalid ID. In this case, we recommend wrapping in an Optional, which we have an implementation for within the API Gateway. Inspired by languages like Swift, this is a nice way of communicating that a response is missing rather than confusing falsy values. We recommend reading the source with its commented usage. It would be even better communicated in a language like TypeScript.

Defense classes and dealing with referential integrity

Optionals are good for communicating missing values, but these can point to a sign of referential integrity problems if encountered frequently. This can be expected in eventually consistent backend services, but it’s still good to keep track. Additionally, some requests may present data that should be stripped out before wrapping in a Response if it is malformed.

We use Defense classes to address this problem. These are classes that should be called from models to filter and report data inconsistencies. Tracking and looking for large numbers of missing items can help you identify areas of bad data in your system. The missing ID defense is common enough that we abstract it and fire statsd events when any IDs were not returned by the API.

Mappers and flow

Once the models are properly returned Response objects, it is up to the resolvers to use these and transform them into the shape of the GraphQL schema. We have a common set of mapper helpers which can be used to perform common translations such as key renaming. These are single arity functions that we like to compose with a tool such as lodash/fp#flow. This pipeline composition is especially convenient when unwrapping optionals.

import { flow, map, filter } from "lodash/fp";
import { mapNameToTitle } from "utility/mappers";

async function resolver(_obj, args, context) {
  const response = await context.models.myModel.index(args);

  return flow(
    filter(optional => optional.isSome()), // remove missing values
    map(optional => optional.must()),      // unwrap
    map(item => item.data()),              // pull out camel-cased data from each Response
    mapNameToTitle()                       // convert name to title to match our GraphQL schema
  )(response);
}

Formatting errors

As of apollo-server@^2, the package now includes custom errors that allow the default error handler to return specific errors for the client to use. This is a great way to allow your clients to determine what to do with the variety of errors they may receive. This reporting is done by way of error extensions.

As Axios can raise API errors, we opt to generically handle those at the request level via formatError so that resolvers do not need to handle those cases in tedious, repetitive try/catch blocks. This also allows client to generically handle specific API errors case by case if they really need to. In general, a simple client error handling plan like this is sufficient.

Tracking

You will probably want to report and track requests in some manner once your API Gateway is in production. Apollo offers their excellent platform and engine for managing performance and error reporting. In addition, if you need to report with your existing tools you can use an extension to hook into the GraphQL request lifecycle for reporting, as documented here. The source package is the best place to see what lifecycle methods you can hook into. We use it to report tracing, response, and error stats over statsd.

Query introspection for look-ahead fetching

Do any of your backend services have APIs that allow you to dynamically fetch columns of data? These can be difficult APIs to work with (and not very cacheable) but are sometimes a reality. If you need to do this, you may need to do schema introspection so that you can add the GraphQL query fields to the respective resolve API fetch.

To do this, you can use the fourth argument info to access the query’s abstract syntax tree (AST). By default, you will be given the current node under info.fieldNodes but also have access to the entire query.

This object is advanced and we recommend reading through the Prisma article “Demystifying the info Argument in GraphQL Resolvers” to go through all the types you need to find. In general, you will dig into fieldNodes recursively through selectionSet.selection to find what you need in the query by name.

As a rule of thumb, looking any further than one node ahead is discouraged as it’s a sign of a bad resolution strategy.

Final Thoughts

Thank you for reading and for checking out the code examples. It simultaneously feels like we’ve gone over so much and yet just scratched the surface of all the fun considerations and ideas with GraphQL and API Gateways. I hope you found the example useful and that you’ll share your thoughts below.

We are hiring! This project was open-sourced by Kapost, a content operations and marketing platform developed in beautiful Boulder, CO. If this project and similar challenges sound fun to you, check out our careers page and join our team!

Nathanael is a software engineer at Kapost. He is passionate about delivering better features and UX by improving front-end architecture, tooling, and education. Follow him on Twitter @NBeisiegel.

Comments 0
There are currently no comments.