Q21 of 37 · API testing

How do you test an API that uses GraphQL?

API testingMidapigraphqlqueriesmutations

Short answer

Short answer: Send POST requests to the single endpoint with a query/mutation in the body; assert on `data` and `errors`. Test query shape (only requested fields return), error handling (errors come back in 200s with an `errors` array), and schema introspection. Use GraphQL Codegen for type-safe test queries.

Detail

GraphQL changes the testing model in three ways: one endpoint instead of many, errors travel inside 200 responses, and clients pick fields explicitly.

The basic pattern:

test('user query returns requested fields', async ({ request }) => {
  const res = await request.post('/graphql', {
    data: {
      query: `query GetUser($id: ID!) {
        user(id: $id) { id email role }
      }`,
      variables: { id: '42' },
    },
  });

  expect(res.status()).toBe(200);
  const { data, errors } = await res.json();
  expect(errors).toBeUndefined();
  expect(data.user).toEqual({
    id: '42',
    email: expect.stringMatching(/@/),
    role: expect.any(String),
  });
});

Key differences from REST:

1. Single endpoint, multiple operations. Everything goes to POST /graphql. Routing is in the query body, not the URL. Tests don't have endpoint-level coverage to worry about.

2. Status code is almost always 200. Even validation errors and authorisation failures often return 200 with an errors array. Test by checking errors, not status code:

expect(errors[0].extensions.code).toBe('UNAUTHENTICATED');

3. Field-level selection. The client picks fields. Test patterns:

  • Query only the fields you need; the response should contain only those.
  • Query a non-existent field → 400 / parse error.
  • Query a field requiring auth → errors with auth code.

4. Mutations. Same shape, different intent:

mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id email
  }
}

5. Subscriptions (websockets). Outside the scope of basic API tests; needs a websocket-aware client.

Test patterns specific to GraphQL:

Schema introspection{ __schema { types { name } } }. Useful in CI to detect schema drift between consumer and provider.

N+1 detection. Some queries trigger N+1 backend queries; tests can assert on response time or via a SQL counter (if your test infra exposes one).

Authorisation per field. Some fields are admin-only. Run the same query with different roles; confirm restricted fields appear or return null.

Persisted queries (for production performance). Many GraphQL APIs only accept pre-registered queries by hash. Tests need to register the query first or use a dev mode that allows ad-hoc.

Tools:

  • GraphQL Codegen — generates TS types from your schema, so test queries are type-safe.
  • GraphiQL / Apollo Sandbox — explore the schema interactively.
  • Pact + GraphQL — contract testing for GraphQL is supported but less mature than REST.

Common pitfalls:

  • Asserting on HTTP 200 = success. GraphQL errors are inside 200 responses.
  • Querying with null variables and not testing the case where the field is genuinely null vs not provided.
  • Ignoring the extensions field, which often carries useful error metadata.

// EXAMPLE

user.graphql.test.ts

import { test, expect } from '@playwright/test';

test('createUser mutation', async ({ request }) => {
  const res = await request.post('/graphql', {
    data: {
      query: `
        mutation CreateUser($input: CreateUserInput!) {
          createUser(input: $input) {
            id email role
          }
        }
      `,
      variables: {
        input: { email: 'alice@example.com', role: 'viewer' },
      },
    },
  });

  expect(res.status()).toBe(200);
  const { data, errors } = await res.json();
  expect(errors).toBeUndefined();
  expect(data.createUser.email).toBe('alice@example.com');
});

// WHAT INTERVIEWERS LOOK FOR

Single-endpoint mental model, asserting on errors-in-200 not on status, awareness of field-level auth and persisted queries, and bonus knowledge of Codegen for type safety.

// COMMON PITFALL

Asserting on res.status() === 200 and concluding the query worked. GraphQL returns errors inside 200 responses; you must check errors as a separate step.