GraphQL at the REST-aurant

A tasty introduction to GraphQL

A screencast version with of this post

It’s Wednesday evening, another great meetup is hosted at my favourite event space: the REST-aurant.

After the talks the attendees can get different food at different “routes”.

Different routes to get salads or burgers at the REST-aurant.

They usually have the same two choices: avocado salad and shrimp burger. Avocado salad, and shrimp burger. But what I really hunger for is Shrimp salad! But unfortunately, there is no GET /salads?with=shrimps route. So what can I do?

I get the shrimp from the burgers, of course!

So I go to the GET /salads route and then queue up to fetch 3 burgers from GET /burgers. I go to a free seat at a table, I pick out the shrimps from the burgers and throw away the rest. And boy am I happy to have my Shrimp salad.

Under- and over-fetching

After I finish my shrimp salad I feel kind of sad. Not only did I waste time standing in the line twice instead of hanging out with my friends, I had to throw away lots of food.

But at least I now have a great example to explain the concepts of under and overfetching. Let’s do some coding!

(You can find all code examples at github.com/gr2m/restaurant-graphql)

Here is a simple Node.js server built with express that exposes two routes for salads and burgers. Both return one salad/burger respectively by default and accept an optional ?count query parameter to fetch more than one with a single request

const express = require(“express”);
const app = express();
// define a salad and a burger
const salad = { avocado: 1, mango: 1, tomato: 0.2, arugula: true, onion: true };
const burger = { buns: 2, shrimp: 1, egg: 1, lettuce: 2.5, mayo: true };
// define arrays of 100 each
const salads = new Array(100).fill(salad);
const burgers = new Array(100).fill(burger);
// define the routes with the optional count query parameter
app.get(“/salads”, ({ query: { count } }, res) => res.json(get(salads, count)));
app.get(“/burgers”, ({ query: { count } }, res) =>
res.json(get(burgers, count))
);
// helper method to get a slice of the array based on count
const get = (what, count) => what.splice(0, parseInt(count) || 1);
// start the server at localhost:4000
app.listen(4000);

Opening http://localhost:4000/salads will look something like this

Firefox renders JSON responses nicely by default

Switching to the client side.

(Note: if you get an error in your browser, please try in Chrome, as it has the best support for async/await which makes the code examples much simpler)

// helper function to send a GET request to given route
function get(path) {
return (await fetch(`${location.protocol}//${location.host}${path}`)).json()
}
let [salad] = await get("/salads");
// salad: {"avocado":1,"mango":1,"tomato":0.2,"arugula":true,"onion":true}
delete salad.tomato;
// TODO: Tell the team to list tomato in the salds route menu!
let burgers = await get("/burgers?count=3");
// burgers: [
// {"buns":2,"shrimp":1,"egg":1,"lettuce":2.5,"mayo":true},
// {"buns":2,"shrimp":1,"egg":1,"lettuce":2.5,"mayo":true},
// {"buns":2,"shrimp":1,"egg":1,"lettuce":2.5,"mayo":true}]
Object.assign(salad, {
shrimps: burgers.reduce(
(numShrimps, burger) => numShrimps + burger.shrimp,
0
)
});
// salad: {"avocado":1,"mango":1,"arugula":true,"onion":true, shrimp: 3}

I get a salad by fetching the GET /salads route. After this first request I’m still lacking ingredients, so I have to send another request. This is what is referred to as under-fetching.

Then I fetch 3 burgers from GET /burgers?count=3. After that I reduce the burgers to the total number of shrimps. 3 shrimp is what I needed for my shrimp salad, but what I received instead was 3 burgers with all ingredients. This is what is referred to as over-fetching.

In summary

  1. Under-fetching
    GET /salads got me the salad, but no shrimps
  2. Over-fetching
    GET /burgers?count=3 got me the shrimps, but I have to throw away the rest of the burgers.

Introducing GraphQL

The REST-aurant team are very kind and environmentally conscious people. They don’t want to see food go to waste, and after some research, they find out about GraphQL, which seems to address that problem perfectly.

For the next meetup, the team sets up a third route: POST /graphql.

There is no need for written menu for GraphQL, instead they setup a terminal with embedded documentation, which the attendees can use to write and post their query. They call it the GraphQL Query-aitor 3000!

GraphQL Query-aitor 3000 is just a fancy name for GraphiQL (note the i), a simple web form to send GraphQL queries with built-in auto-complete. GraphiQL is somewhat hard to pronounce if you don’t know it, so folks give it different names, like GitHub’s explorer. It’s pronounced like “graphical”, by the way :)

The GraphiQL web application

Graphiql shows all available options as you type. There is no more guessing, no more looking up of property names, and most importantly, no more out of sync documentation, because the documentation is generated from the same schema that the server and clients using. For example you see “tomato” in the autocomplete dropdown in GraphiQL while it was missing in the menu for the GET /salads route.

The full query to request all ingredients but tomato from one salad and only the shrimp from 3 burgers looks like this

{
salads {
avocado
arugula
mango
onion
}
burgers(count:3) {
shrimp
}
}

The response from the the server follows the tree structure of my query, and it includes exactly what I asked for, not less, not more.

{
"data": {
"salads": [{"avocado":1,"arugula":true,"mango":1,"onion":true}],
"burgers": [{"shrimp":1},{"shrimp":1},{"shrimp":1}]
}
}

GraphQL: what you need is what you get.

Here are a few facts about GraphQL:

  • Just like REST, GraphQL is a specification, not a tool.
  • It is language agnostic for both servers and clients
  • A GraphQL API is built around a schema
  • A schema is a simple text document and used as contract between client and server.

We will dive into the GraphQL schema for our sample application in a minute. But I want to emphasise that GraphQL is language agnostic as it is commonly seen together with React and Node.js. The reason is that both React and GraphQL are projects by Facebook. While React is an actual JavaScript library, GraphQL is just a specification, there are implementations in many programming languages already.

Now, let’s create a simple text file called schema.graphql. A GraphQL schema has to follow the syntax of said GraphQL specification. For our example app, the entire schema is only 20 lines long:

type Query {
burgers(count: Int = 1): [Burger]
salads(count: Int = 1): [Salad]
}
type Burger {
buns: Int!
shrimp: Float!
egg: Float!
lettuce: Boolean!
mayo: Boolean!
}
type Salad {
avocado: Float!
mango: Float!
tomato: Float!
arugula: Boolean!
onion: Boolean!
}

The Query type defines what can be requested at the root, in this case it is salads and burgers. An optional count integer can be passed. It defaults to 1. You can see that reflected in the GraphQL query shown above.

salads returns an array with items of type Salad. The Salad type defines all its ingredients. For example, avocado is a float number which allows for a decimal point. Same with mango, cucumber and tomato. Onion is a boolean, it can either be true or false.

Burger buns are of type Int, because who wants a burger with half a bun? Shrimp and egg are floats, lettuce and mayo are Booleans.

On the server, the code additions are the following.

const { readFileSync } = require("fs");
const bodyParser = require("body-parser");
const { graphqlExpress, graphiqlExpress } = require("apollo-server-express");
const { makeExecutableSchema } = require("graphql-tools");
const schema = makeExecutableSchema({
typeDefs: readFileSync("schema.graphql", "utf8"),
resolvers: {
Query: {
salads: (_, { count }) => get(salads, count),
burgers: (_, { count }) => get(burgers, count)
}
}
});
app.use("/graphql", bodyParser.json(), graphqlExpress({ schema }));
app.use("/graphiql", graphiqlExpress({ endpointURL: "/graphql" }));

You can see the full code at github.com/gr2m/restaurant-graphql/tree/master/02-graphql.

The bulk of the work is done by two npm modules that we need to install first: apollo-server-express and graphql-tool.

The schema.graphql needs to be turned into a JavaScript representation so it can be processed in the /graphql route handler. I read out the raw file and pass it as typeDefs property to the makeExecutableSchema function.

The other property, resolvers, defines query resolves which work similar to route handlers. I reuse the get helper shown in the first server.js code in the beginning of this post to return a slice of the salads or burgers array respectively, based on the optional count argument.

Then finally I define the GraphQL middleware which exposes the POST /graphql route as well as the GraphiQL web application at /graphiql.

Post the query to the `/graphql` endpoint in the `query` key of a JSON object.

// helper function to send a POST request to given route
async function post(path, data) {
return (await fetch(`${location.protocol}//${location.host}${path}`, {
method: 'post',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
})).json()
}
let {data: {salads: [salad], burgers}} = await post('/graphql', {
query: `{
burgers(count:3) {
shrimp
}
salads {
avocado
arugula
mango
onion
}
}`
})
// salad: {"avocado":1,"mango":1,"arugula":true,"onion":true}
// burgers: [{"shrimp":1},{"shrimp":1},{"shrimp":1}]
Object.assign(salad, {
shrimps: burgers.reduce((numShrimps, burger) => numShrimps + burger.shrimp, 0)
})
// salad: {"avocado":1,"mango":1,"arugula":true,"onion":true, shrimp: 3}

The response returns exactly the ingredients I need, I just need to reduce the burgers to 3 shrimps and assign it to the salad object.

In summary

  • A GraphQL API is usually just another REST endpoint, e.g. POST /graphql
  • A query extracts a data tree to get exactly what’s needed
  • the server response matches the tree structure of query

Yay! I can enjoy my avocado shrimp salad in good conscience now.

Persisted queries

At the next meetup, everyone wants to try out the GraphQL Query-aitor 3000. And the result is a very long line

Long lines at the GraphQL terminal

Getting exactly what we want is great, but entering the query each time takes much longer then just fetching a burger from GET /burgers. And after sending the POST /graphql request, the server has to process the new order on the spot.

The REST-aurant team comes together again and thinks about a solution to make the POSTing and processing of the GraphQL queries more efficient. And they come up with a solution: remembering queries! Each time someone posts a query they get asked if they want to have it remembered for next time and receive a reference ID in return.

The team also sends an email ahead of the next event asking attendees to pre-register their queries. With such up-front information, the team can already prepare some of the food, which also reduces the processing time.

Persisted queries are query strings stored with a unique ID in a key/value store. In our server example I’ll create a persisted-queries.js which defines a single query with the id 1:

module.exports = {
1: `{
burgers(count:3) {
shrimp
}
salads {
avocado
arugula
mango
onion
}
}`
};

On the server, I have to add a few lines to load the persisted-queries.js file and add middleware that checks if an id property was POSTed

const persistedQueries = require("./persisted-queries");
app.use("/graphql", bodyParser.json(), (req, res, next) => {
if (persistedQueries[req.body.id]) {
req.body.query = persistedQueries[req.body.id];
}
next();
});

If an id has been was POSTed and if a persisted query with the passed ID exists, set the query for that ID from the persisted queries store and continue the query processing as before.

I no longer need to post the query, instead I just post my id, which is one. To get the same result as shown above in the browser, I no longer need to send the full query:

let {data: {salads: [salad], burgers}} = await post(‘/graphql’, {
id: 1
})

The result is the same as before, but the size of the request is minimal in comparison. This can have big performance impacts as GraphQL query can become very complex and therefore the request becomes big. And the upstream connection is usually much worse than the down stream connection, too.

For the REST-aurant team, it is great to know queries ahead of time. The queries can be pre-compiled and data can be better cached. Some GraphQL APIs even go as far as disable non-persisted queries altogether to improve security and maximise efficiency.

In summary

  • Persisted Queries are not part of the GraphQL specification but a common implementation detail
  • Persisted Queries are stored on the server, clients only send query IDs
  • Sending only an ID instead of a complex query reduces the request size
  • Persisted queries can be pre-compiled on the server
  • To improve security, non-persisted queries can be disabled altogether

Mutations

The REST-aurant is growing in popularity and more often than not the event runs out of food before everyone could get a bite. So far, 100 salads and 100 burgers were delivered by a caterer, but the team decides that they will hire a cooking team that can create more burgers and salads to meet the growing demand.

Mutations are part of the GraphQL specification and have to be defined
in the GraphQL Schema. It’s only 4 additional lines

type Mutation {
addBurgers(count: Int = 1): Int
addSalads(count: Int = 1): Int
}

First, I add a mutation to add salads. The mutation accepts an optional count argument which defaults to 1. The mutation will return an integer which will be the total amount of available salads. The same for burgers.

A GraphQL query which adds a salad and 3 burgers looks like this:

mutation {
addSalads
addBurgers(count: 3)
}

A mutation has to start with the mutation keyword. You can send multiple mutations in a single request, another benefit over REST APIs. You cannot combine a mutation with a query, but you can define the mutation response if you want to.

For the above query, the response will look like this

{
"data": {
"addSalads": 1,
"addBurgers": 3
}
}

In the server code we now set salads and burgers to empty arrays and amend the resolvers object with a Mutation property:

// start with empty salad & burger arrays now
const salads = [];
const burgers = [];
// add mutation resolvers
const schema = makeExecutableSchema({
typeDefs: readFileSync("schema.graphql", "utf8"),
resolvers: {
Query: {
salads: (_, { count }) => get(salads, count),
burgers: (_, { count }) => get(burgers, count)
},
Mutation: {
addSalads: (_, { count }) => {
salads.push(...new Array(count).fill(salad));
return salads.length;
},
addBurgers: (_, { count }) => {
burgers.push(...new Array(count).fill(burger));
return burgers.length;
}
}
}
});

The addSalads and addBurgers function are called with the count argument which defaults to 1 as defined in the schema. The add one or multiple salads/burgers based on the count argument. Both functions then return the length of the respective array.

Sending a mutation from the browser is very similar to sending a query:

await post('/graphql', {
query: `mutation {
addSalads
addBurgers(count: 3)
}`
})

In summary

  • Mutations are used to create, update or delete data.
  • Multiple mutations can be sent with a single request. They are processed sequentially.
  • Mutations can’t be combined with Queries, but the mutation responses can be filtered

GraphQL subscriptions

When the REST–aurant is out of salad or burgers, it is rather annoying to be in front of the line and ask repeatedly: “can have my salad yet?”. Instead, I want the server to tell me when there is enough food available to fulfill my query. This is one common use case for subscriptions.

Subscription is the third operation type of GraphQL. GraphiQL has built-in
support for subscriptions over websockets, they can be submitted just like
like queries and mutations. Once submitted, the server responds that the data will appear once there is a change. So let’s make changes!

Subscription on the left shows updates from mutations on the right.

I leave the subscription as is in one browser while POSTing mutations in a second one. Immediately after I post the mutations, the numbers on
the first window are changing accordingly.

Let’s see how the implementation looks like.

The additions to schema.graphql are again fairly simple:

type Subscription {
foodAdded: Stats
}
type Stats {
burgers: Int
salads: Int
}

The foodAdded subscription will be called with type Stats which has Integer properties for the total number of available salads and burgers

The additions to the server are a bit more complex:

// load additional libraries needed for subscriptions
const { execute, subscribe } = require("graphql");
const { SubscriptionServer } = require("subscriptions-transport-ws");
const { PubSub } = require("graphql-subscriptions");
const pubsub = new PubSub();
const getStats = () => ({ salads: salads.length, burgers: burgers.length });
const schema = makeExecutableSchema({
typeDefs: readFileSync("schema.graphql", "utf8"),
resolvers: {
Query: {
salads: (_, { count }) => get(salads, count),
burgers: (_, { count }) => get(burgers, count)
},
Mutation: {
addSalads: (_, { count }) => {
salads.push(...new Array(count).fill(salad));
// publish the foodAdded and pass the stats
pubsub.publish("foodAdded", { foodAdded: getStats() });
return salads.length;
},
addBurgers: (_, { count }) => {
burgers.push(...new Array(count).fill(burger));
// publish the foodAdded and pass the stats
pubsub.publish("foodAdded", { foodAdded: getStats() });
return burgers.length;
}
},
// add the Subscription property to schema resolvers
Subscription: {
foodAdded: {
subscribe: () => pubsub.asyncIterator("foodAdded")
}
}
}
});
// add the subscriptionsEndpoint to the /graphiql middleware
app.use(
"/graphiql",
graphiqlExpress({
endpointURL: "/graphql",
subscriptionsEndpoint: "ws://localhost:4000/subscriptions"
})
);
// instead of app.listen(4000), create a new server instance
const server = createServer(app);
server.listen(4000);
// ... which can be passed to the SubscriptionServer constructor
new SubscriptionServer(
{ schema, execute, subscribe },
{ path: "/subscriptions", server }
);

You can see the full code at https://github.com/gr2m/restaurant-graphql/tree/master/05-graphql.

A GraphQL subscription is pushing data to the client once a change occurs instead of the client pulling it based on an interval waiting for a change to happen. At the core of our implementation are the modules subscriptions-transport-ws and graphql-subscriptions. The latter gives us PubSub which we use to publish the foodAdded event in both of our mutations. Then in the Subscription resolver, we return an asynchronous iterator which pushes the passed data to the foodAdded event trough the open web socket connection. We pass the compiled schema to the new SubscriptionServer which implements the web socket route.

I know it’s quite a lot to take in, but it’s also hella cool once it’s working :)

In summary

  • When waiting for changes, instead of requesting data based on an interval, you subscribe to a data stream.
  • The server publishes data as soon as it becomes available.

Hungry?