«

Lessons learned wrapping a REST API with GraphQL

I've recently went down the rabbit-hole wrapping a massive API with FaceBook's GraphQL specification. Aside from learning a ton about my company's API's, I walked away with a lot of lessons on good conventions doing this work.

Learn from my pain and read below.

1. Automate your type's

Writing out type definition by hand is pretty tedious, especially with larger API's that include numerous routes. If you can leverage something like swagger to automate defining your types, then spend the time doing so! I sunk a few days writing some code to bootstrap these types based on our API's meta information, and was able to automatically stand up 87 services. I'll say that again, in 2 days I was able to stand up 87 services based on existing meta information already available.

Sadly, there's no conventional way to automate (at runtime or otherwise) your type definitions, so you'll have to either keep these types up-to-date manually or write more tooling to help. The upside to having these types be static is, that having done so, you get a lot of really cool stuff for free.

2. Define your modules carefully

If the API your graph'ing together has an implied hierarchy, it'll definitely be worth your time to write code in a way that makes it more shareable. Break up each API route into its own resource definition that includes a 4 exports:

  1. type with just field-level definitions.
  2. uri for the resource's route/location.
  3. type export for that resource as a collection, including paging arguments.
  4. mutation for resources that have write/delete operations.

Type Export

A type is essentially a, well, type definition for all the fields that your resource contains. By exporting this type by itself, without any paging or other meta information, you can consume it in other parts of your Graph's Scheme. Consider below:

// parent.js
export const type = new GraphQLObject({  
  name: 'parent',
  fields: () => ({
    name: {
      type: GraphQLString,
    },
    id: {
      type: GraphQLID,
    },
  });
});

// ... later in child.js
import { type as parentType } from './parent';

export const type = new GraphQLObject({  
  name: 'child',
  fields: () => ({
    name: {
      type: GraphQLString,
    },
    id: {
      type: GraphQLID,
    },
    parent: {
      type: parentType,
    },
  }),
});

Because we expose the rudimentary type in parent.js, we can easily reuse all of those fields here for child.js. This method tends to work best for one-to-one relationships since we're not dealing with paging, filtering, or sorting.

You might have also noticed that field's in both these types are functions and not object-literals (which they seem to be in many other examples). The reason that we use functions is so that we can have circular dependencies:

// parent.js with child type
import { type as childType } from './child';

export const type = new GraphQLObject({  
  name: 'parent',
  fields: () => ({
    name: {
      type: GraphQLString,
    },
    id: {
      type: GraphQLID,
    },
    children: new GraphQLList(childType),
  });
});

Since both of these files require each other you must define your fields as functions since node.js lazily loads circular dependencies.

URI Export

While fairly straightforward, this can be easily missed: You'll definitely want to export where the resource is located. Later, when we're writing resolveing functions, not having to guess or assume where a resource is located will be necessary. Plus it makes things like logging trivial when problems arise.

Type Export as a Collection

Not just merely a GraphQLList of a particular type, a type collection should define the resource with paging and any other meta information. Building on our child definition from above, let's export a collection type that will contain:

export const collection = new GraphQLObject({  
  name: 'childrenCollection',
  fields: () => ({
    children: new GraphQLList(type),
    paging: { // ... your paging stuff from API },
  }),
  args: {
    parentId: {
      type: GraphQLID,
    },
  },
});

Now your parentType can use this type and support paging through children and more.

Mutation Export

There's really not much here to write home about, you'll have to export a mutation in order for consumers to mutate/create new instances of your resource.

I'll say that you can combine this with other mutation's to composes atomic operations, but things can get a bit tricky with that, so be careful there!

3. Write resolve helpers

resolve is how we fulfill your type definition with the actual json that will be used for the response. resolve takes 4 arguments, which I've named differently than what I've seen elsewhere: function resolve(parent, args, context, execution) . I won't go too much into all of the parameters, but will cover the first 3 as they are the most important:

parent

This is the JSON response from the object surrounding whatever thing you're trying to resolve. Looking back at our child/parent example earlier, this would look like:

// child.js with parent type
import { parentType } from './parent';

const childType = new GraphQLObject({  
  name: 'child',
  fields: () => ({
    name: {
      type: GraphQLString
    },
    id: {
      type: GraphQLID
    },
    parent: parentType,
  }),
  resolve: function (parent, args) {
    // when used by `parent.js` `parent` will have
    const parentId = parent.id;
    const parentName = parent.name;
  }
});

It's important to highlight that the parent object returns raw responses prior to any resolve's. Let's say we have this:

const parentType = new GraphQLObject({  
  name: 'child',
  fields: () => ({
    name: {
      type: GraphQLString
    },
    id: {
      type: GraphQLID
    },
    lastName: {
      type: GraphQLString,
      resolve: (obj) => obj.last_name, // The API returns `last_name`, but we map it to `lastName` for consistency
    },
  })
});

In child.js, you'd have to access this property by referencing parent.last_name as opposed to parent.lastName in resolve!

args

args are the things being passed in by, well, the arguments of the query. For instance:

{
  child (first: 10) {
    id,
    name
  }
}

To access first: 10, you'd have to use the args argument to grab the key/value:

const childType = new GraphQLObject({  
  name: 'child',
  fields: () => ({
    name: {
      type: GraphQLString,
    },
    id: {
      type: GraphQLID,
    },
    parent: parentType,
  }),
  resolve: function (parent, args) {
    // when used by `parent.js` `parent` will have
    const first = args.first;
  },
});

This is mostly useful when paging through a collection, as consumers will pass in the paging requirements this way.

context

context is a malleable property that can be defined when you mount your GraphQL Schema in Express. This is useful if you want to pass in tokens or instances of something at request-time and have it shared to all of your queries:

// When mounting GraphQL
router.use(graphqlHTTP((req, res) => ({  
  schema,
  context: {
    token: req.headers.Basic, // Grab the token from headers
    api: new API(), // Share an API instance for all resolvers
  },
})));

Now, in your resolvers you can find these fields simply by access the context argument:

const childType = new GraphQLObject({  
  name: 'child',
  fields: () => ({
    name: {
      type: GraphQLString,
    },
    id: {
      type: GraphQLID,
    },
    parent: parentType,
  }),
  resolve: function (parent, args, context) {
    const token = context.token;
    const api = context.api;
  },
});

Building the resolve helper

With all this in mind, we can finally get to writing a resolve helper! Let's say we have some code that fetch's things via an API:

// API.js
export class API {  
  constructor(token) {
    this.token = token;
  }

  get(uri, queryParams) {
    return fetch(`${uri}${queryParams}`, {
      headers: {
        Basic: this.token,
      },
    });
  }
}

Let's also set this up at request time so we can re-use this helper in all our requests:

import { API }  from 'api.js';

router.use(graphqlHTTP((req, res) => ({  
  schema,
  context: {
    // Share an API instance for all resolvers
    api: new API(req.headers.Basic),
  },
})));

Now we can write a pretty nice utility that can build a resolver with all these piece in place. We'll use a library called qs to handle parameterizing our options.

import qs from 'qs';

export const generateResolver = (uri) {  
  return (parent, args, context) => {
    const { api } = context;
    const qsParams = qs.stringify(args);

    return api.get(uri, qsParams);
  };
};

Finally, let's use all this and easily build resolvers for our hypothetical child.js:

import { generateResolver } from './resolve-helpers';

const uri = '/child';

const childType = new GraphQLObject({  
  name: 'child',
  fields: () => ({
    name: {
      type: GraphQLString,
    },
    id: {
      type: GraphQLID,
    },
    parent: parentType,
  }),
  resolve: generateResolver(uri),
});

Attaching the child to parent is trivial now that we have a nice resolve utility:

// parent.js
import { generateResolver } from './resolve-helpers';

import { uri as childUri, type as childType } from './child';

export const uri = '/parent';

export const type = new GraphQLObject({  
  name: 'parent',
  fields: () => ({
    name: {
      type: GraphQLString,
    },
    id: {
      type: GraphQLID,
    },
    children: {
      type: new GraphQLList(childType),
      resolve: generateResolver(childUri),
    },
  }),
  resolve: generateResolver(uri),
});

Closing comments

After having these concepts in your toolbox, and understanding a bit more on the inner-working, hopefully you'll have everything you need to wrap your own API's in GraphQL. If you're looking for ways to build a UI with GraphQL, I've really enjoyed using the Apollo Client. Not only is there a strong lack of magic in their technology; the folks on their slack channel are very friendly and open questions and comments.

Thanks for reading, and good luck building your shiny-new GraphQL service!

Share Comment on Twitter