GraphQL Persisted Queries using GET requests.

This is part 1, you can also find part 2 here, or part 3 here.

We recently decided to update one of our micro-services using GraphQL. The app sits between our new mobile web client and our Discovery API service. The purpose of the app is to model our data and increase performance by managing the number of service calls the client needs to make on the front-end. We utilize edge caching to avoid unnecessary trips to the server allowing requests to be intercepted and returned early from memory. We were well aware of the queries the client would send beforehand, so we also wanted to utilize GraphQL persisted queries in an attempt to save bandwidth between the client and server. Since GraphQL sends POST requests to a single endpoint, edge caching becomes difficult to accomplish. This is where GET request can help us. In this tutorial we’ll create a GraphQL server that accepts GET requests for persisted queries.

UPDATE: the tech world moves fast. This tutorial series starts off using Apollo Server 1.3.2, but don’t worry, in part three we’ll go over Apollo Server 2 and the entire migration process. Till then, if you have any trouble following along, you can download the original project files here: source files.

To get started, create a working directory and install the following packages using npm.

npm init -y &&
npm install --save graphql graphql-tools apollo-server-express express graphql-playground-middleware-express body-parser ramda

We’ll need to create a schemas file and a resolvers file

touch schemas.js resolvers.js

schemas.js

const Greeting = `
type Greeting {
name: String
text: String
}
`
const Query = `
type Query {
greeting(name: String): Greeting
}
`
module.exports = [Greeting, Query]

resolvers.js

const resolvers = {
Query: {
greeting: (_, { name }) => ({
name,
text: 'How are you today?'
})
}
}
module.exports = resolvers

Next, we need to create an extracted queries file. This will be used as a hash map for the client. Instead of sending over an entire query document, we’ll allow the client to send over query parameters instead. The query params will contain a key called hash whose value will correspond to a key in our extracted queries file. In other words, instead of ordering a burger, fries, and a drink, our client will just order the number 1. { hash: 1 }

touch extracted_queries.js

Inside the extracted_queries.js file we’ll export an object. The key will be a hash (in our case a simple integer) and the value will be the desired query.

extracted_queries.js

module.exports = {
1: `query Greeting($name: String!) {
greeting(name: $name) {
name
text
}
}`
}

Next, we’ll need to create some middleware to check all incoming requests for a hash in the query params.

touch persistedQueries.js

If a hash exists, we’ll retrieve the matching key in our extracted queries file. If it doesn’t exist, we’ll send the request through untouched.

persistedQueries.js

const { omit } = require('ramda')
const queryMap = require('./extracted_queries.js')
const persistedQueries = (req, res, next) => {
const { hash = '' } = req.query

if (!hash) return next()
  const query = queryMap[hash]

if (!query) {
res.status(400).json({ error: [{}] })
return next(new Error('Invalid query hash'))
}
  req.query = {
query,
variables: omit(['hash'], req.query)
}
  next()
}
module.exports = persistedQueries

In the persistedQueries function we begin by destructuring the hash property off the req.query object setting the default value to an empty string. We then check for a truthy value in the hash prop, if one is not provided, we simply return next() sending the request to the next upcoming middleware function.

If a hash exists, we use it to pattern match against our extracted queries file. We reassign the req.query object with the matching query and use ramda’s omit method to remove the hash key before calling next() , sending the request on its way. If a match does not exist we return a status of 400 and call next(...), passing in an error message that will later be logged and reviewed. Now all we need to do is create a server.js file to tie it all together.

touch server.js

server.js

const express = require('express')
const bodyParser = require('body-parser')
const playground = require('graphql-playground-middleware-express').default
const { graphqlExpress, graphiqlExpress } = require('apollo-server-express')
const { makeExecutableSchema } = require('graphql-tools')
const persistedQueries = require('./persistedQueries')
const typeDefs = require('./schemas')
const resolvers = require('./resolvers')
const port = 4000
const app = express()
const schema = makeExecutableSchema({ typeDefs, resolvers })
app.use(
'/graphql',
bodyParser.json(),
persistedQueries,
graphqlExpress({ schema })
)
app.use(
'/playground',
playground({ endpoint: '/graphql' })
)
app.listen(port, () => console.log(`listening on port: ${port}`))

We create a simple express app and combine our typeDefs and resolvers by using graphql-tools makeExecutableSchema function. We use express app.use method to declare a route based middleware. The first argument is a string containing our desired route /graphql . To keep things backwards compatible we pass in the body-parser middleware, which we’ll need for any POST requests the server may receive. Next, we pass in our persistedQueriesmiddleware which will only make changes to the request if it spots a hash in the req.query params. The last parameter is the graphqlExpress middleware from apollo-server-express. This takes an object with the combined results from our makeExecutableSchema call above.

We also create a route for the GraphQL in browser IDE GraphQL Playground. We’ll use this to test our POST routes to insure we haven’t broken the GraphQL server’s default functionality.

Now let’s start the server with the following command.

node server.js

If you see the message listening on port: 4000 in the terminal then you’re successfully up and running. Open up your browser and navigate to http://localhost:4000/playground. You should see the graphql-playground IDE. Copy and paste the query from our extracted queries file into graphql-playground, and run the query by clicking the play icon. Don’t forget to add the name variable in the variables section at the bottom like so…

If the value of name is null, you may have forgotten to add the name variable on the bottom left side of the IDE

If done properly you should see the results of your query as a JSON response.

{
"data": {
"greeting": {
"name": "Ticketmaster",
"text": "How are you today?"
}
}
}

Now open a new tab in your browser and navigate to http://localhost:4000/graphql?hash=1&name=Ticketmaster. You should see the same JSON response as the previous tab, but this time we’re using query parameters instead of sending an entire query document to the server.

Success! Now we get a performance boost from our GraphQL persisted queries and edge caching from our GET request URIs.

Extra Reading Material

We hand waved over a lot of different packages that do a lot of cool things. Here are some links to a few of the honorable mentions