Getting started with GraphQL: what client to use?
When I first started working with GraphQL APIs my first challenge was to decide what GraphQL frontend library I wanted to use. I can remember spending all morning exploring all sorts of options, from small libraries like graphql-request to slightly larger ones like urql and finally the most well known like Apollo. These are all great libraries - in fact we use urql at work - but at this point in time I was working with a tiny GraphQL library that I'd built for a side project and I really didn't need any complexity. I think I lost a good couple of hours trying to decide before thinking: what if I made my own?
This post is not meant to criticise libraries: they provide a bunch of features that many applications will want and need, but if you're just getting started, they might be overkill for your needs.
Do you need a library to use GraphQL?
I had in my head this mindset that making a request to a GraphQL API was "special" and not something that I could do with the fetch
API, for example. I'm not really sure where this came from but I think I'd seen so many talks about Apollo and various client libraries doing all sorts of smart things I'd ended up assuming that I'd use one of those. But Apollo packs in a vast array of features that I really didn't need on my side project. I wanted to make a request and get the data. Concerns such as smart caching and cache invalidation were not present for me.
When you're starting to learn something it can be tempting to reach for libraries to fill in gaps in knowledge but I highly recommend trying to avoid doing this when possible. I'm very happy that I made the decision to write my own tiny client because it plugged gaps in my knowledge and de-mystified how a GraphQL API works. In this post I'll talk through how to get started talking to a GraphQL API just by using the fetch
API and nothing more.
A sample GraphQL API
We need a sample API for this and I've made one that lives on Heroku: http:faker-graphql-api.herokuapp.com/graphql. This API returns some fake people (all data is generated by Faker.js). It lets us query for people and get their names:
{
people {
name
}
}
Returns an array of ten people and their names. This is the query we're going to use as our example today.
My dummy API is hosted on a free Heroku instance so please be patient if it takes some time to boot up when you request it.
Making a request to a GraphQL API
It turns out there are some simple steps to follow to talk to a GraphQL endpoint:
- All requests are
POST
requests - You should pass the
Content-Type
header asapplication/json
- The body of the request should contain a string which is the GraphQL query
As long as we follow those rules we can easily use fetch
to talk to the API. Let's do it!
const api = 'http:faker-graphql-api.herokuapp.com/graphql'
export const request = ({ query }) => {
return fetch(api, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
}),
})
.then(response => response.json())
.then(result => {
console.log('got here!', result)
return result
})
}
The request
function takes an object and expects the query
key to contain the raw GraphQL query. The fetch
API takes the URL and an object of options, which are used to configure the request: we set method: 'POST'
and the Content-Type
header as discussed and then use JSON.stringify({ query })
to generate the body for the request, passing in the query
that was passed in to our request
function. Finally, the GraphQL API will return JSON so we parse the response before returning it (I've logged it just to aid debugging but feel free to skip that!).
With that we can make our request:
request({
query: `{ people { name } }`,
})
And you should get some people back! ๐.
If you only need to make basic requests in your app you could stop here and be done. We've saved having to install, learn and ship in our bundle any additional libraries. Of course this comes with less functionality - but for some projects that might be just fine.
If you do need caching and more advanced features I'd highly recommend a well tested, established library rather than rolling your own!
Supporting variables
Another feature of GraphQL is that queries can take variables. For example, the fake API lets us find a single person by their ID:
query fetchPerson($id: Int!) {
person(id: $id) {
name
}
}
To support this our API needs to pass variables through as well that it includes in the request:
export const request = ({ variables, query }) => {
return fetch(api, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
})
.then(response => response.json())
.then(result => {
console.log('got here!', result)
return result
})
}
And now our client supports variables:
request({
query: `query fetchPerson($id: Int!) {
person(id: $id) {
name,
}
}`,
variables: {
id: 1,
},
})
If this is all you need, or you're not using React for your frontend, you can stop here. This client will be plenty good enough to keep you going as you work with and get more familiar with GraphQL. By working with your own implementations first you'll find you have a greater fundamental understanding when swapping to a library, and you'll understand the features the library provides better.
A React hook!
Finally let's see how easy it would be to wrap this up in a React hook for those of you working with React.
If you're not familiar with hooks, I wrote an introduction to them which will help get you up to speed.
Creating the hook is a case of wrapping our request
function in a React.useEffect
hook and storing the response via React.useState
:
export const useGraphQL = ({ variables, query }) => {
const [data, setData] = React.useState(null)
React.useEffect(
() => {
request({ variables, query }).then(setData)
},
[variables, query]
)
return [data]
}
This hook is missing some useful features like tracking if we're loading or not, but I'll leave that as an exercise to the reader ๐
We can use this hook within a component like so:
const [data] = useGraphQL({
query: `{ people { name } }`,
})
And it works! There is one gotcha though that I want to highlight. If you do this:
const [data] = useGraphQL({
variables: {},
query: `{ people { name } }`,
})
You'll cause an infinite loop of requests, which isn't what we want! This is because React.useEffect
has variables
as a dependency and every time it changes it will cause the effect to re-run. Every re-render this code runs and variables: {}
creates a new object every time which means React.useEffect
will re-run.
We can fix this by remembering to wrap our variables
in a React.useMemo
hook to ensure that we only recalculate the variables if we need to:
const vars = React.useMemo(
() => {
return {
id: props.id,
}
},
[props.id]
)
const [data] = useGraphQL({
variables: vars,
query: `{ people { name } }`,
})
But this requires you to remember to do this every time. Instead what we can do is convert the variables
within our useGraphQL
hook to a string, via JSON.stringify
, and use that as the dependency to useEffect
:
const stringifiedVars = JSON.stringify(variables)
React.useEffect(
() => {
request({ variables, query }).then(setData)
},
[stringifiedVars, query]
)
โ๏ธThis isn't the best solution but it is the easiest and will serve just fine for most projects. It's also similar to how the popular urql works although that uses the fast-json-stable-stringify to avoid some of the performance problems with
JSON.stringify
.
Conclusion
Although this post has focused on GraphQL I hope that your main takeaway is to resist diving straight for libraries. You can often get a long way with a few lines of code you write yourself, particularly when learning a new technology. This will help your understanding of the tech that you're learning but also your understanding of libraries: if you've written a library yourself, however small and straight forward, you're more likely to be able to follow how the more complex libraries work.