GraphQL on the server
Introduction
In the following post we'll quickly explain what GraphQL is and why you might want to consider it. GraphQL is a querying language for APIs, and we have been using it as an alternative to REST.
Why not use REST you may wonder? There are a few advantages to using GraphQL. The first is that it helps abstract some of the data constructs which can be particularly helpful in your frontend models. Another advantage is one can limit the payload data to only get the data you need rather than parse out the entire payload after transport. This is particularly important in a mobile dominated world where saving every bit of bandwidth is a value-add. Also useful to mobile, you can get many resources in a single request (batch) which helps avoid loading of multiple REST URLs. Last, GraphQL can help ease the burden of maintainability when your data does change.
This post, however, isn't meant to be an introduction to GraphQL. Instead, we will show you through examples and discussion of more advanced topics related to building a GraphQL server and how GraphQL has been useful for us. Specifically, we will be writing a server in Node.js that is designed to be constructed by multiple engineers. Therefore, it might be helpful to familiarize yourself with the basics of GraphQL. A good place to start is an introduction to GraphQL or how to GraphQL.
The examples used in the following post come from our GraphQL wrapper for the Joyent CloudAPI. The wrapper itself is a hapi plugin that relies on graphi, which is a hapi plugin designed to make building GraphQL servers in hapi a straightforward task.
Simple example of a GraphQL request
Before we look at the server code we need to have an understanding of what a GraphQL schema looks like and how to interact with it from a clients perspective. GraphQL requests are simple to construct with tools that you already have in your toolkit. For example, you can make GraphQL requests to a server simply using curl
. Additionally, there is an Node.js module named GraphiQL that is an interactive IDE designed to construct queries and execute them against a server. This is the tool that we tend to use when building out more sophisticated queries.
We will look at the virtual machine section of the example data object from the GraphQL wrapper. The following schema represents a virtual machine. It is simplified and incomplete, but will illustrate some example object types. Even though the following is a snippet from the overall schema, it actually contains enough data to be understood by the GraphQL JavaScript library.
type Machine { # Unique id for this machine id: ID # The "friendly" name for this machine name: String # The current state of this machine (e.g. running) state: MachineState # The image this machine was provisioned with image: Image}enum MachineState { PROVISIONING RUNNING STOPPING STOPPED DELETED}type Image { # Unique id for this image id: ID # The "friendly" name for this image name: String}
In addition to defining the various object types, a schema should also define what methods are supported. In GraphQL the methods are contained in a type definition for either Query
, Mutation
, or Subscription
. For the purposes of this example, the Query
type will only contain a single method to retrieve a machine
, as shown below:
type Query { machine(id: ID): Machine}
From the above information we are now able to construct the following query to retrieve the name of a machine.
query { machine(id: "SOME-UUID" ) { name } }
We can even convert this into an HTTP request using curl, which might look similar to the following:
curl -X POST http://localhost/graphql -H "Content-Type: application/json" -d '{ "query": "query { machine(id: \"SOME-UUID\" ) { name } }" }'
The response will be JSON and contain something similar to the following structure:
{ "data": { "machine": { "name": "foo" } }}
GraphQL supports multiple queries being sent as part of a single request, which is the reason why the machine result being contained in the data
object. Later in this post we will provide an example of submitting multiple queries in a single request. If the query for a machine is successful, then the result will be contained in a field with the key of the query, which is machine
. If there is an error, the result will be contained in an errors
field.
Handling a GraphQL request on the server
When describing the underlying function that processes a HTTP request, most languages and frameworks will use the term handler. The handler function has varying responsibilities depending on what framework or design patterns are used, but generally speaking, it's responsible for performing some action or providing the appropriate response to whatever it was asked about. In GraphQL, instead of using the term handler, the specification uses the term resolver. Therefore, when you read through the following code snippets and see the term resolver
, think of it as an operation handler.
In order to help make it easy to get started with GraphQL in Node.js—particularly with existing hapi developers—we have created a hapi plugin called graphi. When registering graphi, you will either provide it with a list of resolvers that align to the queries and mutations found in the GraphQL schema, or you can let graphi map hapi routes as resolvers. For example, if we want to provide a resolver for retrieving a machine for a particular id
, then we can create a resolver like the following:
const getMachine = function ({ id }) { return { id, name: 'foo', state: 'RUNNING', image: {} // populate with image properties };};
As you can see, the getMachine function expects a single argument id
and will return an object matching the schema for a machine. Resolvers are usually asynchronous operations, therefore it's perfectly safe to return a promise, which will be awaited on.
In order to utilize this with hapi, we can register the graphi plugin and reference the resolver and schema, as shown below:
const Graphi = require('graphi');const Hapi = require('hapi');// read schema from separate .schema fileconst main = async () => { const server = Hapi.server({ port: 8080 }); const resolvers = { machine: getMachine // from previous example }; await server.register({ plugin: Graphi, options: { schema, resolvers } }); await server.start();};main();
After the server is started, a new route is added which accepts requests found at the /graphql
path. The endpoint is capable of accepting the following request and responding with the following response.
$ curl -X POST "http://localhost:8080/graphql" -H "Content-Type: application/json" -d '{ "query": "query { machine(id: \"84f5497f-11f7-4e09-890a-58dd3517b685\") { name } }" }'{"data":{"machine":{"name":"foo"}}}
The interesting thing to note is that we retrieved and populated the image
in the resolver, even though the client didn't ask us for information about the image
. This is a carry-over design from how we would prepare responses in a REST world. However, now that we are using GraphQL and we can see exactly what fields the client is concerned with receiving, we can remove this extra processing. We can do this by using the resolver formatters feature which graphi supports. Alongside the individual query and mutation resolvers that we populate on the resolvers
option, we can provide an object for each type defined in the schema for providing further formatting.
For example, if we want to provide a function to execute whenever a client needs an image for a machine, we can create a Machine
object and include it as another resolver as shown in the example below.
const Machine = { image: function ({ id }) { return { id, name: 'bar' } }};const resolvers = { machine: getMachine, Machine};
Whenever the previous request is sent to the /graphql
endpoint, the image
function isn't executed.
However, if the following query is received, then the image function is executed and the appropriate image is returned:
$ curl -X POST "http://localhost:8080/graphql" -H "Content-Type: application/json" -d '{ "query": "query { machine(id: \"84f5497f-11f7-4e09-890a-58dd3517b685\") { name, image { name } } }" }'{"data":{"machine":{"name":"foo","image":{"name":"bar"}}}}
Additionally, if we want to combine multiple queries into a single request we can. In order to demonstrate this ability, we will use the GraphQL interface which is provided by default when you register graphi. After the previous hapi server is started, you can launch http://localhost:8080/graphiql in a browser to load the GraphiQL interface. Using the GraphiQL interface, you can execute GraphQL requests in a much simpler way than using cURL.
In the example below we will execute two queries to retrieve two different machines. However, because we are executing the same query multiple times, we will need to differentiate them in the results. Fortunately, GraphQL supports aliasing operations, which we will use for distinguishing the queries. Below is an example of running multiple queries in a single request.
Summary
There are many reasons to consider adopting GraphQL, especially for new services that have multiple client applications. The flexibility of retrieving only the fields a client needs, as well as the ability to alias or rename fields, mean that every client is able to retrieve data in a manner that is tailored for their needs without any updates to the server code. Additionally, the fact that multiple queries or mutations can be batched together into a single request make for a compelling case to adopt GraphQL.
There are additional, more advanced features that graphi provides. Most notably, graphi supports mapping existing hapi REST based routes to GraphQL resolvers by simply changing their method from POST
or GET
to GRAPHQL
. Further, graphi also supports merging multiple schemas and resolvers together, in order to make it easier for multiple teams to build GraphQL plugins that are composable in a single hapi server.
Hopefully, this example will encourage further exploration of the GraphQL ecosystem and open up a world of new possibilities. We've certainly enjoyed adopting GraphQL for many of our new services here at Joyent and hope you'll have reasons to give it a try in yours.
Post written by Wyatt Preul, Lloyd Benson