Trying GraphQL

Starting with GraphQL - from zero to hero.

Defining schema

You start with a schema - a description of what the clients can ask. For example if we have a "product" item we can describe like this below.

1
2
3
4
5
6
7
8
const typeDefs = `
"describes Product item"
type Product {
_id: ID
name: String! # name is required
qty: Int
}
`

For now this is a simple JavaScript string.

Simple queries

It is not enough to describe the Product schema. We also should describe queries that return it. Let us return a single product by id, or list of all products. I am also putting comments into the schema string as quoted lines or after # character. You can find more schema examples in the graph-tools repo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const typeDefs = `
"describes Product item"
type Product {
_id: ID
name: String!
qty: Int
}

"""
a couple of queries to get a single product
or an array of all products
"""
type Query {
getProduct(_id: ID): Product
allProducts: [Product]
}
`

Resolvers

Each query should return actual data somehow. We need to map each query to a resolver function. Let us use hard coded data for now. Here is our data

1
2
3
4
5
6
7
8
9
const products = [{
id: 1,
name: 'foo',
qty: 10
}, {
id: 2,
name: 'bar',
qty: 3
}]

Here are the two resolvers we need - one for allProducts and another for getProduct. These methods map by name to the properties inside type Query in our type definitions.

1
2
3
4
5
6
7
8
9
10
11
12
const resolvers = {
Query: {
// simple resolver - returns array of products
allProducts () {
return products
},
// grab the argument "id" by deconstructing the second argument
getProduct (_, {id}) {
return products.find(p => p.id === id)
}
}
}

Server

We now can use graphql-tools to make middleware for an Express.js server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import express from 'express'
import graphqlHTTP from 'express-graphql'
import { makeExecutableSchema } from 'graphql-tools'
// typeDefs + resolvers => schema
const schema = makeExecutableSchema({
typeDefs,
resolvers
})

const app = express()
const PORT = 3000
// put graphQL at /g endpoint
app.use(
'/g',
graphqlHTTP({
schema,
graphiql: true,
})
)

app.listen(PORT, () => {
console.log(`server running at ${PORT}`)
})

Once we start the server, we can either the graphical interface (open your browsers at localhost:3000) or do regular GET requests from the command line. I am using excellent httpie client.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
http http://localhost:3000/g?query={allProducts{id}}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"data": {
"allProducts": [
{
"id": 1
},
{
"id": 2
}
]
}
}

Excellent - the result is placed into data key, and we have only asked for ids. Let us ask for more fields; we can ask for name and qty of each product. Note that from the command line I need to escape commas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ http http://localhost:3000/g?query={allProducts{id\,name\,qty}}
{
"data": {
"allProducts": [
{
"id": 1,
"name": "foo",
"qty": 10
},
{
"id": 2,
"name": "bar",
"qty": 3
}
]
}
}

If I ask for a non-existent property, instead of data field, the server will return a list of errors.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ http http://localhost:3000/g?query={allProducts{foo}}
HTTP/1.1 400 Bad Request
{
"errors": [
{
"locations": [
{
"column": 14,
"line": 1
}
],
"message": "Cannot query field \"foo\" on type \"Product\"."
}
]
}

Let us ask for a specific item (and we only ask for id and name properties).

1
2
3
4
5
6
7
8
9
$ http http://localhost:3000/g?query={getProduct\(id:1\){id\,name}}
{
"data": {
"getProduct": {
"id": 1,
"name": "foo"
}
}
}

It is kind of annoying to escape special URL symbols, like , and () when making these requests from command line.

Graphql files

Instead of using strings, we can place our GraphQL schema definitions into .graphql files and use a helper module to import them. I will use graphql-import to import the schema file (and the default VSCode syntax highlighting)

schema.graphql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"describes Product item"
type Product {
id: Int! # integer id property is required
name: String! # name string is required
qty: Int # integer quality is optional
}

"""
a couple of queries to get a single product
or an array of all products
"""
type Query {
getProduct(id: Int!): Product
allProducts: [Product]
}
1
2
3
import { importSchema } from 'graphql-import'
const typeDefs = importSchema('./schema.graphql')
// works the same way

We can even split the schema file further. Place "Person" definition in person.graphql

person.graphql
1
2
3
4
5
6
"describes Product item"
type Product {
id: Int! # integer id property is required
name: String! # name string is required
qty: Int # integer quality is optional
}

and import "Person" using import in the comment from schema.graphql

schema.graphql
1
2
3
4
5
6
7
8
9
10
# import Person from "person.graphql"

"""
a couple of queries to get a single product
or an array of all products
"""
type Query {
getProduct(id: Int!): Product
allProducts: [Product]
}

and now the GraphQL schemas are organized much better.

Connecting query to the database

Next, we need to fetch real data from the database. I went through the Build GraphQL APIs with Node.js on MongoDB Egghead.io course, coding along. You can find the working version in the bahmutov/graphql-node-mongo-egghead-course repository. The master branch has a local MongoDB database (via Mongoose ORM). As an experiment, I also implemented the database API by using objection.js ORM on top of sqlite3. Check out branch objection-1 to see the code. Here is a typical resolver:

resolvers.ts
1
2
3
4
5
6
7
8
9
10
11
12
import Product from './models/product' // objection model
export const resolvers = {
Query: {
async allProducts() {
return await Product.query()
},

async getProduct(_, { _id }) {
return await Product.query().findById(_id)
},
}
}

Quite nice - each resolver can be a Promise-returning function that maps nicely to the database query.

Mutations

Having just static queries is not enough. How do we write mutations using GraphQL that add new data items or update existing ones? We write type Mutation type definition! For example, here are typical queries for adding / deleting / updating a Product type.

1
2
3
4
5
6
7
8
9
10
input ProductInput {
name: String!
qty: Int!
}
type Mutation {
createProduct(input: ProductInput): Product
updateProduct(_id: ID, input: ProductInput): Product
"when deleting an item, just return the ID"
deleteProduct(_id: ID): ID
}

The implementation in resolvers is very similar to the queries.

resolvers.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export const resolvers = {
// Query ..
Mutation: {
// creating a single product
async createProduct(_, { input }) {
return await Product.query().insert(input)
},

// updating a product by ID
async updateProduct(_, { _id, input }) {
return await Product.query().patchAndFetchById(_id, input)
},

async deleteProduct(_, { _id }) {
await Product.query().deleteById(_id)
return _id
}
}
}

Perfect 🎉

Apollo Server v2

While you can use any HTTP server with GraphQL endpoint, it makes sense to try something that is optimized for serving GraphQL queries. Apollo Server is one such thing. Its v2 promises to be pretty good, so I am trying a release candidate (npm i apollo-server@rc). You can run its stand alone like this

1
2
3
4
5
6
7
8
9
const { ApolloServer, gql } = require('apollo-server')
const typeDefs = gql`
# your type definitions
`
const resolvers = {} // your resolvers
const server = new ApolloServer({ typeDefs, resolvers })
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

Or you can apply ApolloServer v2 as middleware to existing server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require('express')
const { ApolloServer, gql } = require('apollo-server')
const typeDefs = gql`
# your type definitions
`
const resolvers = {} // your resolvers
const server = new ApolloServer({ typeDefs, resolvers })

const app = express()
const PORT = 4000
server.applyMiddleware({ app })

app.listen(PORT, () => {
console.log(`server running at ${PORT}`)
console.log(`try GraphQL playground at ${PORT}/graphql`)
})

If you get an API key from a Apollo organization, you can enable performance tracing on your server which is very important, because all queries go through the same endpoint, and so "traditional" performance monitoring is not going to work very well.

Performance

You can turn on performance tracing in some GraphQL servers. For example

1
2
3
4
5
const server = new ApolloServer({
...schema,
tracing: true,
cacheControl: true,
})

Then make a query request like

1
$ http :3000/graphql?query={hello

and you should see performance information with the result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"data": {
"hello": "Welcome to G"
},
"extensions": {
"cacheControl": {
"hints": [],
"version": 1
},
"tracing": {
"duration": 2193735,
"endTime": "2018-06-21T20:06:47.368Z",
"execution": {
"resolvers": [
{
"duration": 1495979,
"fieldName": "hello",
"parentType": "Query",
"path": [
"hello"
],
"returnType": "String!",
"startOffset": 666848
}
]
},
"startTime": "2018-06-21T20:06:47.366Z",
"version": 1
}
}
}

This is what ApolloEngine uses to show performance stats for the GraphQL queries.

Links

There are many excellent resources for learning GraphQL. Some of the ones I read are

Extras

I have several other blog posts that are trying a technology. Check out these links