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.
type
‘sWriting 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 type
s 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.
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:
type
with just field-level definitions.uri
for the resource’s route/location.type
export for that resource as a collection, including paging arguments.mutation
for resources that have write/delete operations.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 type
s 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.
While fairly straightforward, this can be easily missed: You’ll definitely want to export where the resource is located. Later, when we’re writing resolve
ing 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.
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:
children
property.paging
object that’ll hold the API’s paging info.args
property that’ll let users request the children by parent.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.
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!
resolve
helpersresolve
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 resolve
rs 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;
},
});
resolve
helperWith 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),
});
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!