DEV Community

Andrew Petersen
Andrew Petersen

Posted on • Updated on

Build a Bare-Bones GraphQL Client w/ Fetch

There are already plenty of great browser based GraphQL clients. Check out, Exploring GraphQL Clients, for a solid rundown of the existing options.

So why would you write your own GraphQL Client?

  • Maybe it's for a tiny thing and you don't want to deal with pulling in NPM packages and bundling code.
  • Or you're writing a Chrome Snippet
  • Maybe, like me, you just want to go through the motions to learn more about GraphQL.

The purpose of this post isn't to create a competing/better GraphQL client. The 10 minutes it takes to craft our own could never compete with the features and robustness of the existing libraries.

The Basics

At minimum, a client-side request to a GraphQL endpoint needs to be a POST, with a query property in the Request body. The GraphQL server will respond with JSON in the form of either { data } or { errors }.

GraphQL Request

  • HTTP POST
  • POST body contains a query property
  • query is a valid GraphQL string

GraphQL Response

  • JSON in the shape of { data, errors }
  • data, if success, will be an object that contains a property for each of the things you asked for in your GraphQL query.
  • errors, if there are any, will be an array of objects. Each error object should at least have a message property.

An example request

In this example we connect to a publicly available Pokemon GraphQL API and console.log the result of a query.

// Public Pokement API
const ENDPOINT = "https://graphql-pokemon.now.sh";
// Use a tool like GraphiQL to help you craft your query
const POKEMON_QUERY = `
  query TopFivePokemon {
    pokemons(first:5) {
      name
      image
    }
  }
`;

// Call fetch, first passing the GraphQL endpoint, then the Request Options
fetch(ENDPOINT, {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  // The request body should be a JSON string
  body: JSON.stringify({ query: POKEMON_QUERY })
})
  // Handle a JSON response
  .then(res => res.json())
  .then(result => console.log(result));

In the above snippet we:

  • Use JavaScript string templates to make the GraphQL query readable
  • Tell the server we are sending JSON with the Content-Type header
  • Set our POST body to be the JSON string of our { query } object.
  • Expect the server to return JSON, and handle the response accordingly with res.json()

Console Output

{
  "data": {
    "pokemons": [
      {
        "name": "Bulbasaur",
        "image": "https://img.pokemondb.net/artwork/bulbasaur.jpg"
      },
      {
        "name": "Ivysaur",
        "image": "https://img.pokemondb.net/artwork/ivysaur.jpg"
      },
      {
        "name": "Venusaur",
        "image": "https://img.pokemondb.net/artwork/venusaur.jpg"
      },
      {
        "name": "Charmander",
        "image": "https://img.pokemondb.net/artwork/charmander.jpg"
      },
      {
        "name": "Charmeleon",
        "image": "https://img.pokemondb.net/artwork/charmeleon.jpg"
      }
    ]
  }
} 

Basic Client Factory

The above snippet worked great, but let's enhance things slightly so that our GraphQL endpoint is configurable and we don't rely on some random ENDPOINT variable existing.

We want to create an instance of a GraphQL client that is tied to a specific endpoint.

The Factory Pattern is just a fancy way of saying, "make a function that creates instances of things". It's an alternative to new'ing up a class, createClient(endpoint) instead of new Client(endpoint)

Let's shoot for an API that looks like this:

// Create an instance of a GraphQL client by passing in your endpoint
let client = createClient("https://graphql-pokemon.now.sh");

// Make an actual GraphQL request by passing a GraphQL query 
// to the request method on the client
client.request(POKEMON_QUERY)
  .then(result => console.log(result));

To implement:

  1. Take the fetch code from the first example and turn it into a function named request.
  2. Wrap request with another function, createClient, to create a closure around our endpoint parameter.
function createClient(endpoint) {

  let request = function(query) {
    return fetch(endpoint, {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      // The request body should be a JSON string
      body: JSON.stringify({ query })
    }).then(res => res.json());
  };

  return { request };
};

Client Factory with Custom Headers

Now that our endpoint is configurable lets add a little more flexibility by allowing developers to pass in HTTP headers that should be included on all requests (common for an Authorization header).

  • We let our createClient function take in a second parameter for headers and default it to an empty object.
  • We spread the passed in headers onto the headers property of our Fetch options.
// Allow an optional second param for request headers
function createClient(endpoint, headers = {}) {

  let request = function(query) {
    return fetch(endpoint, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        // spread the passed in headers
        ...headers
      },
      body: JSON.stringify({ query })
    }).then(res => res.json());
  };

  return { request };
}

GraphQL Variables

The last thing we want to support are variables to tokenize our GraphQL queries (and mutations). In the screenshot below you can see that the name is getting set via a variable.

GraphQL Variable

This is actually pretty easy to support. All we need to do is add an additional property to our POST body named variables.

However, instead of only adding support for just variables, let's have request take in an optional second param, generically named queryOptions. Then we'll spread whatever queryOptions are given (in our case, variables) onto the POST body.

function createClient(endpoint, headers = {}) {

  // Take in an optional queryOptions object
  let request = function(query, queryOptions = {}) {
    return fetch(endpoint, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        ...headers
      },
      // spread the passed in queryOptions onto the POST body
      body: JSON.stringify({ query, ...queryOptions })
    }).then(res => res.json());
  };

  return { request };
}

And that's it. Our final client factory can support configuring custom headers as well as GraphQL variables.

Final Usage example:

const POKEMON_BY_NAME_QUERY = `
query GetByName($name:String!) {
  pokemon(name:$name) {
    types 
    name
    image
    height {
      minimum
      maximum
    }
  }
}`;

let client = createClient("https://graphql-pokemon.now.sh", {
  Authorization: "Bearer ABC123"
});

async function getPokemonByName(name) {
  let {data,errors} = await client.request(POKEMON_BY_NAME_QUERY, { variables: { name } })
  if (errors) {
    console.log("Uh Oh!", errors.map(e => e.message));
  }
  return data.pokemon
}


getPokemonByName("Pikachu");

Top comments (0)