Consuming GraphQL Simply

Get Started Without a Consumer Library

I’ve recently found myself once again building an API gateway service — a server which provides to consumers a cohesive, unified interface to a suite of specialized backend concerns. The first official client is a mobile app, and early in the project we decided GraphQL seemed an ideal interface with the phone. No one on our team had practical experience with it, so we decided to make GraphQL an eventual ideal.

GraphQL provides a number of benefits to a service like the one I’m building. It forces you to think about your data entities and their relationships up front. It allows the API consumer expressive flexibility in deciding what it wants, without worrying overmuch about which resources are where.

The GraphQL schema providing library I’m using in the server provides tools to map nodes in the graph to resolvers which are responsible for getting what’s needed when it’s needed without complication. I recently started migrating much of the gateway service’s internals over to resolvers, and making the REST endpoints execute GraphQL under the hood. It’s been such a great experience it’s how I want to build any REST APIs in the future.

I setup an instance of GraphiQL to demo the new API. But while working with the mobile team to transition from our ad-hoc REST endpoints to using the new technology, there’s not a lot of resources to understand how to work with a GraphQL API aside from, just use a library. While that might be a good idea, the popular libraries tend to be quite heavyweight, solve problems not common to all implementations of GraphQL (including ours), and tend to be prescriptive about client-side state management.

If you just want to understand how to work with it there’s not a lot of beginner-friendly info. The documentation can be a bit abstract at times, and often conflates the topics of providing, consuming, resolving and responding to GraphQL queries. GitHub has a good guide, but unless you’re working with their API it’s not apparent that their resource is a general one.

Let’s get started by making some simple queries against GitHub’s extensive GraphQL API and API Explorer using GraphiQL, let’s make some simple queries. Any demo involving HTTP ought to provide examples using curl.

First, create a personal token for use on the command-line. In the examples below, this will be $TOKEN. I’ll prettify the results a bit. Let’s use GitHub’s example query to see if it works:

whoami

POST /graphql HTTP/1.1
Authorization: bearer $TOKEN
Content-Type: application/json; charset=utf-8
Host: api.github.com
{"query":"query { viewer { login }}"}
curl -H "Authorization: bearer $TOKEN" -X POST -d "\
{ \"query\": \"query { viewer { login }}\" }" \
https://api.github.com/graphql
query { viewer { login }}
{ "data": {
"viewer": {
"login": "mattly" }}}

Indeed it did! A few things for your attention:

  1. The query is a POST request with a JSON body. This is standard for GraphQL over HTTP. Don’t argue about REST semantics here — POST is for the query because it might be too large to fit in url query parameters.

  2. The query is a string at the query field inside the request body. The API server will parse the string into the query structure.

  3. The query body is not valid JSON. GraphQL queries are their own language and many editors can highlight them specially.

  4. The data we wanted [queryRoot viewer login] is in the response body at the data key.

While that covers the basics, you really should know about variables, mutations, and errors.

A GraphQL schema can declare that a field takes arguments, and sometimes those arguments are required for certain things. For example, if I wanted to list my top repositories, GitHub requires me to specify how many I want per-page via a field argument:

popular repos

POST /graphql HTTP/1.1
Authorization: bearer $TOKEN
Content-Type: application/json; charset=utf-8
Host: api.github.com
{"query":"query {
viewer {
repositories(
first: 3,
orderBy: { field: STARGAZERS, direction: DESC }
) { nodes { name }}}}"}
curl -H "Authorization: bearer $TOKEN" -X POST -d "\
{ \"query\": \
\"query { \
viewer { \
repositories( \
first: 3, \
orderBy: {field: STARGAZERS, direction: DESC}) \
{ nodes { name }}}}\" }" \
https://api.github.com/graphql
query {
viewer {
repositories(
first: 3,
orderBy: {
field: STARGAZERS,
direction: DESC }) {
nodes {
name }}}}
{ "data": {
"viewer": {
"repositories": {
"nodes": [
{ "name": "vim-colors-pencil" },
{ "name": "bork" },
{ "name": "iterm-colors-pencil" }]}}}}

In the repositories field, I’m telling it I want the first four, ordered by the number of stars. The first field is required by this API, and orderBy defaults to the creation time. I found all this out by looking at GitHub’s GraphiQL Explorer and poking around at the documentation (generated by introspection on the schema) on the right of the interface. It also told me that orderBy is a RepositoryOrder object, which has two fields, both of which whose possible values are part of an enumerated, finite set.

On the other hand, if I only want to know how many repositories I have without knowing anything else about them, I don’t have to provide a count:

total repo count

POST /graphql HTTP/1.1
Authorization: bearer $TOKEN
Content-Type: application/json; charset=utf-8
Host: api.github.com
{"query":"query { viewer { repositories { totalCount }}}"}
curl -H "Authorization: bearer $TOKEN" -X POST -d "\
{ \"query\": \
\"query { \
viewer { \
repositories { totalCount }}}\"}" \
https://api.github.com/graphql
query {
viewer {
repositories {
totalCount }}}
{ "data": {
"viewer": {
"repositories": {
"totalCount": 43 }}}}

Requiring a value for the repositories field or not is a requirement the server enforces based on whether you’re reaching further into the nodes on that part of the graph.

A useful feature of GraphQL is variables. If you wanted to query for any given person’s top-starred repositories, you could construct such a query with string concatenation. Or perhaps a dynamic GraphQL query-generation library. You shouldn’t — you should use variables instead.

variables

POST /graphql HTTP/1.1
Authorization: bearer $TOKEN
Content-Type: application/json; charset=utf-8
Host: api.github.com
{"query":"query UserMostStarredRepos($user: String!) {
user(login: $user) {
repositories(
first: 3,
orderBy: { field: STARGAZERS, direction: DESC }) {
nodes { name stargazers { totalCount }}}}}",
"variables": {"user": "mattly"}}
curl -H "Authorization: bearer $TOKEN" -X POST -d "\
{ \"query\": \
\"query UserMostStarredRepos(\$user: String!) { \
user(login: \$user) { \
repositories( \
first: 3, \
orderBy: {field: STARGAZERS, direction: DESC}) \
{ nodes { name stargazers { totalCount }}}}}\",
\"variables\": {\"user\": \"mattly\"}}" \
https://api.github.com/graphql
query UserMostStarredRepos($user: String!) {
user(login: $user) {
repositories(
first: 3,
orderBy: {
field: STARGAZERS,
direction: DESC }) {
nodes {
name stargazers { totalCount }}}}}
{ "data": {
"user": {
"repositories": {
"nodes": [
{ "name": "vim-colors-pencil",
"stargazers": { "totalCount": 344 }},
{ "name": "bork",
"stargazers": { "totalCount": 211 }},
{ "name": "iterm-colors-pencil",
"stargazers": { "totalCount": 104 }}]}}}}

There’s a few new things here:

  1. I gave a operation name for the query – UsersMostStarredRepos – along with variables for it. The variable, user is a string, and must be present (that’s what the ! means). Adding an operation name is required when you have variables.

  2. I used this variable in the arguments to the user field.

  3. There’s a variables value in the request body which is a JSON object.

Eventually you’ll want to submit changes to the API. In the REST world, we have a number of ways to do that: POST, PUT, and since some people were being a little too pedantic about that behavior we have PATCH, and of course DELETE.

But rarely have I met an API that sticks to the straight semantics behind these REST verbs. People bend the rules such that “REST” becomes RPC over HTTP. GraphQL offers a better way with mutations. And they’re not very different from what we’ve seen so far:

  1. Mutations don’t use the Query root but rather Mutation root, and so have different entry points from regular queries.

  2. If you provide multiple mutations in a single query, they are performed in the order given in the query.

  3. Mutations take input via arguments, and return objects, from which you must select at least one field.

This query will do one new thing though, use an inline fragment to select fields on a Union Type:

mutations

POST /graphql HTTP/1.1
Authorization: bearer $TOKEN
Content-Type: application/json; charset=utf-8
Host: api.github.com
{"query":"mutation StarRepo($id: ID!) {
addStar(input: {starrableId: $id}) {
starrable {
... on Repository { name }
viewerHasStarred
stargazers { totalCount }}}}",
"variables": {"id": "MDEwOlJlcG9zaXRvcnkxMDg4OTA0MA=="}}
curl -H "Authorization: bearer $TOKEN" -X POST -d "\
{ \"query\": \
\"mutation StarRepo(\$id: ID!) { \
addStar(input: {starrableId: \$id}) { \
starrable { \
... on Repository { name } \
viewerHasStarred
stargazers { totalCount }}}}}\",
\"variables\": \
{\"id\": \"MDEwOlJlcG9zaXRvcnkxMDg4OTA0MA==\"}}" \
https://api.github.com/graphql
mutation StarRepo($id: ID!) {
addStar(input: {starrableId: $id}) {
starrable {
... on Repository { name }
viewerHasStarred
stargazers { totalCount }}}}
{ "data": {
"addStar": {
"starrable": {
"name": "bork",
"viewerHasStarred": true,
"stargazers": { "totalCount": 211 }}}}}

Here’s what’s going on:

  1. The addStar mutation requires an starrableId value from a Starrable — Gist or Repository. You can get this from the Starrable via its id field.

  2. The addStar mutation returns an returns an addStarPayload object, which has a starrable field. This is a Union Type, since both Repositories and Gists are starrable. In fact, they implement the starrable interface, which is essentially a set of common fields across the implemented types. On Starrable, these fields include stargazers and viewerHasStarred.

  3. Since I also wanted the Repository’s name (to show you what you’ll star if you copy & paste this query), I had to use an inline fragment to get a field not included in Starrable, even though both Repository and gist have name fields.

If the id variable looks a bit funny, run it through a base-64 decoder. It’s common in GraphQL APIs to make IDs opaque in this manner, such that they can represent any complex data the server needs for referencing particular things while discouraging the client from worrying about the particulars.

The last thing we should look at is error handling. Because queries are often asking for more than one thing at a time, GraphQL’s philosophy is to isolate the errors and return as much data as possible. I couldn’t replicate this behavior predictibly with GitHub’s API – queries that required pagination info, for example, did not return any data whatsoever – so instead here’s an example with an easy fix:

errors

POST /graphql HTTP/1.1
Authorization: bearer $TOKEN
Content-Type: application/json; charset=utf-8
Host: api.github.com
{"query":"query UserInfo($user: ID!) {
user(login: $user) {
login name bio company location websiteUrl createdAt
repositories { totalCount }
viewerCanFollow viewerIsFollowing }}",
"variables": {"user": "mattly"}}
curl -H "Authorization: bearer $TOKEN" -X POST -d "\
{ \"query\": \
\"query UserInfo(\$user: ID!) { \
user(login: \$user) { \
login name bio company location websiteUrl createdAt \
repositories { totalCount } \
viewerCanFollow viewerIsFollowing }}\", \
\"variables\": {\"user\": \"mattly\"}}" \
https://api.github.com/graphql
query UserInfo($user: ID!) {
user(login: $user) {
login name bio company location websiteUrl createdAt
repositories { totalCount }
viewerCanFollow viewerIsFollowing }}
{ "data": null,
"errors": [{
"message":
"Type mismatch on variable $user and argument login (ID! / String!)",
"locations": [{
"line": 1,
"column": 41 }]}]}

This should cover enough of the basics to get you going. I’d spend some time playing around with GitHub’s GraphQL Explorer to get a feel for how expressive GraphQL queries can be. Look at the introspected documentation, and write your own queries. Here’s an example query that will look at who you’re following, how many followers they have, what languages they use in their featured repositories, as well as who’s following you, whether you’re following them, what languages are used in their pinned repositories, and how many people they’re following. It could be the base for a little personal dashboard, and it’s just a single, concise query.

query {
viewer {
following(first: 100) {
nodes {
login
pinnedRepositories(first: 10) {
nodes { name languages(first: 10) { nodes { name }}}}
followers { totalCount }}}
followers(first: 100) {
nodes {
login
viewerIsFollowing
pinnedRepositories(first: 10) {
nodes { name languages(first: 10) { nodes { name }}}}
repositories { totalCount }
following { totalCount }}}}}

If you’re seriously considering using GraphQL in your client application – and I do recommend it – I’d suggest checking out a consumer library such as Apollo or Relay if you’re using JavaScript. These solve problems not common to all GraphQL APIs or use cases, can be prescriptive about how you deal with data in your app, and certainly involve more overhead and ceremony than simply using plain HTTP.

Further Reading

The Github GraphQL API

Our new Projects feature is a good example of this: the UI on the site is powered by GraphQL, and you can already use the feature programmatically. Using GraphQL on the frontend and backend eliminates the gap between what we release and what you can consume.

GraphQL Best Practices

While there’s nothing that prevents a GraphQL service from being versioned just like any other REST API, GraphQL takes a strong opinion on avoiding versioning by providing the tools for the continuous evolution of a GraphQL schema.

Colophon

Consuming GraphQL Simply was first published by on . All images (where applicable, unless otherwise noted) and text © 2021. It is published under the Creative Commons Attribution-Noncommercial-Share Alike 4.0 license. If you wish to reproduce any of this content in a commercial context, explicit permission is required. Please contact me directly.

Copyright © 2021Matthew Lyon