GraphQL at the REST-aurant

A tasty introduction to GraphQL

Gregor
Gregor
Feb 2, 2018 · 13 min read
A screencast version with of this post
Different routes to get salads or burgers at the REST-aurant.

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.

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);
Firefox renders JSON responses nicely by default
// 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}
  1. 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.

The GraphiQL web application
{
salads {
avocado
arugula
mango
onion
}
burgers(count:3) {
shrimp
}
}
{
"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:

  • 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.
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!
}
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" }));
// 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}
  • A query extracts a data tree to get exactly what’s needed
  • the server response matches the tree structure of query

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
module.exports = {
1: `{
burgers(count:3) {
shrimp
}
salads {
avocado
arugula
mango
onion
}
}`
};
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();
});
let {data: {salads: [salad], burgers}} = await post(‘/graphql’, {
id: 1
})
  • 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.

type Mutation {
addBurgers(count: Int = 1): Int
addSalads(count: Int = 1): Int
}
mutation {
addSalads
addBurgers(count: 3)
}
{
"data": {
"addSalads": 1,
"addBurgers": 3
}
}
// 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;
}
}
}
});
await post('/graphql', {
query: `mutation {
addSalads
addBurgers(count: 3)
}`
})
  • 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 on the left shows updates from mutations on the right.
type Subscription {
foodAdded: Stats
}
type Stats {
burgers: Int
salads: Int
}
// 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 }
);
  • The server publishes data as soon as it becomes available.

Hungry?

JavaScript Scene

JavaScript, software leadership, software development, and related technologies.

Thanks to JS_Cheerleader and Eric Elliott.

Gregor

Written by

Gregor

Open Source Community facilitator

JavaScript Scene

JavaScript, software leadership, software development, and related technologies.