Notes

A GraphQL 200 is not a pass

November 2026

With a REST API, a 200 usually means the request worked. With GraphQL it often only means the request arrived. A GraphQL server can answer 200 with an errors array in the body, sometimes alongside partial data. A test that checks the status code and stops there will pass while the query fails.

So I assert in three steps, in this order:

  • Status. The HTTP request succeeded.
  • Errors. The body has no errors field, so GraphQL reported no problems.
  • Data. The values in data are the ones we expected.

Here is the idea in Playwright, against the public Countries API. It sends a named query with a required $code variable:

test('country query returns Canada', async ({ request }) => {
  const response = await request.post('https://countries.trevorblades.com/', {
    data: {
      query: `query GetCountry($code: ID!) {
        country(code: $code) { name capital currency }
      }`,
      variables: { code: 'CA' },
    },
  });

  // 1. the request worked
  expect(response.status()).toBe(200);
  const body = await response.json();

  // 2. GraphQL reported no errors
  expect(body.errors).toBeUndefined();

  // 3. the data is right
  expect(body.data.country).toMatchObject({
    name: 'Canada', capital: 'Ottawa', currency: 'CAD',
  });
});

The order matters for the failure message. If the server returns errors, the test fails on step two and prints them, instead of failing on step three with a confusing “cannot read properties of null”.

What this doesn’t cover

This is one successful query. It doesn’t reproduce a 200-with-errors response, and it says nothing about permissions, invalid input or mutations. Error status codes also vary between servers and kinds of error, so don’t assume every GraphQL failure comes back as 200 either. The check above simply refuses to treat 200 as proof.

The full spec is in my playwright-lab repo.