Using Fetch as your GraphQL client

Using Fetch as your GraphQL client

The Fetch API is a core primitive part of today's web standards that replaces XMLHttpRequest.

It adds support for promises and aims to provide a consistent API with Request and Response objects that make working with fetch the same experience, no matter the runtime.

If you've made a request on the server or client over the last few years you no doubt have installed packages like axois that handle making a request.

This library and dozens more abstracted the complexity of handling XMLHttpRequest and built a simpler API that developers could use to make requests, handle promises, and avoid callback hell.

The DX these libraries brought to developers is now thankfully part of the native web API.

fetch('YOUR_API_URL')
  .then(res => res.json())
  .then(data => console.log(data))
  .error(err => console.log(err))

One of the biggest benefits of using Fetch is that it's already part of your runtime. If you're using Deno, Node, the browser or something else, you can use the same API to make a GraphQL request.

There's no need to install any dependencies or configure clients if you don't need to leverage any client-side caching — which is becoming more and more redundant with modern web frameworks that render on the server and handle caching for you.

We will create a Grafbase backend and run it locally using the CLI.

In the root of your project run the following command:

npx grafbase init

You can now start the GraphQL API (and example schema) using:

npx grafbase dev

A GraphQL API (like Grafbase) expects the request to contain some details to help execute the operation.

  • URL
  • Headers
  • Body

The URL is a required argument for fetch. We will provide a second argument that specifies that:

  • The HTTP method as POST
  • The content-type is application/json
  • Body is stringified JSON
const query = `
  query GetTodos {
    todoCollection(first: 100) {
      edges {
        node {
          id
        }
      }
    }
  }
`

fetch('http://localhost:4000/graphql', {
  method: 'POST',
  headers: {
    'content-type': 'application/json'
    // Other headers, including `authorization` or `x-api-key`
  },
  body: JSON.stringify({
    query
  })
})

To pass variables with the GraphQL request using the Fetch API we can update body to include the variables object:

const query = `
  query GetTodos($first: Int) {
    todoCollection(first: $first) {
      edges {
        node {
          id
        }
      }
    }
  }
`

fetch('http://localhost:4000/graphql', {
  method: 'POST',
  headers: {
    'content-type': 'application/json'
    // Other headers, including `authorization` or `x-api-key`
  },
  body: JSON.stringify({
    query,
    variables: {
      first: 100
    }
  })
})

Calling fetch returns a Promise. This means you will need to await or chain callback functions to handle the response or errors.

fetch('http://localhost:4000/graphql', {
  method: 'POST',
  headers: {
    'content-type': 'application/json'
    // ...
  },
  body: JSON.stringify({
    query,
    variables: {
      first: 100
    }
  })
})
  .then(res => res.json())
  .then(data => console.log(data))
  .error(err => console.log(err))

Using the exact same code above you change the contents of the query variable to include a GraphQL mutation:

const query = `
  mutation CreateNewTodo($title: String!) {
    todoCreate(input: {
      title: $title
    }) {
      todo {
        id
      }
    }
  }
`

Typically inside of the first callback you would check if the response is ok. If it is you would then call res.json() and do what you need with the results...

Since GraphQL errors are returned in the JSON response, we will need to get those from the response and log them accordingly.

We'll update the request to destructure data and errors from the Promise res.json() returns:

fetch('http://localhost:4000/graphql', {
  method: 'POST',
  headers: {
    'content-type': 'application/json'
    // ...
  },
  body: JSON.stringify({
    query,
    variables: {
      first: 100
    }
  })
})
  .then(res => res.json())
  .then(({ data, errors }) => console.log({ data, errors }))
  .error(err => console.log(err))

If you now execute an invalid query you will get a message inside of the the errors array.

Libraries like axios and got still provide a wide range of features that you would have to manually implement using the Fetch API. If you have to deal with retrying network requests, refreshing tokens, or intercept requests, the native Fetch API might not be for you.

Handling errors is also something you will need to consider when working with the Fetch API.

If you're going to be using GraphQL subscriptions or live queries with something like EventSource you will need to implement some extra logic with these requests to correctly handle the content type and responses.