Consuming GraphQL Simply

Get Started Without a Consumer Library

Mar 12, 2018 Matthew Lyon

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:

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:

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.

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:

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:

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.

Consuming GraphQL Simply Matthew Lyon 2023 — license: CC BY-NC-SA 4