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 amessage
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 ofnew 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:
- Take the
fetch
code from the first example and turn it into a function namedrequest
. - Wrap
request
with another function,createClient
, to create a closure around ourendpoint
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 forheaders
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.
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)