GraphQL API Testing: Queries, Mutations, Errors and Security

GraphQL API Testing: Queries, Mutations, Errors and Security

GraphQL APIs look simple from the outside, with one URL and one POST, but they hide a surprising number of testing traps. Errors come back with HTTP 200, “not found” is a null instead of a 404, and a single query can ask for so much nested data that it slows the whole server down. In this lesson you’ll test a real public GraphQL API with Postman, cURL and Playwright, and learn the checklist that catches GraphQL-specific bugs.

Quick answerTo test a GraphQL API, send POST requests to its single endpoint with a JSON body containing query (and optional variables and operationName). Verify the data object and the errors array, not just the HTTP status, because validation errors and missing records usually return HTTP 200. Test queries, mutations, variables, nullability, field-level authorization, pagination, query depth/complexity limits and that introspection is disabled in production where required.
Advertisement

GraphQL in 2 Minutes

Real-life example: a restaurant buffet vs a fixed thali A REST endpoint is a fixed thali: GET /users/1 gives you every item on the plate whether you want it or not. GraphQL is a buffet: you pick exactly what goes on your plate, { user(id: 1) { name city } }, and nothing else. Great for the customer, but the kitchen must handle someone piling up 50 plates at once.
  • One endpoint, usually POST /graphql.
  • Schema with types and fields defines what can be asked (strongly typed).
  • Query reads, mutation writes, subscription streams updates (usually over WebSocket).
  • Response is JSON with data and/or errors.

Comparison with REST and SOAP: REST vs SOAP vs GraphQL.

The Public API We’ll Test

We’ll use the free public Countries GraphQL API at https://countries.trevorblades.com/. It needs no key and returns country data. (It’s a third-party demo service; be gentle with it and don’t load test it.)

query {
  country(code: "IN") {
    name
    capital
    currency
    languages { name }
  }
}

Real response (HTTP 200):

{
  "data": {
    "country": {
      "name": "India",
      "capital": "New Delhi",
      "currency": "INR",
      "languages": [ { "name": "Hindi" }, { "name": "English" } ]
    }
  }
}
Advertisement

Sending GraphQL Requests: cURL and Postman

cURL

curl -s https://countries.trevorblades.com/ \
  -H "Content-Type: application/json" \
  -d '{"query":"{ country(code: \"IN\") { name capital currency } }"}'

With variables (the right way)

Never build queries by gluing strings together; pass values as variables. It’s cleaner and avoids injection.

{
  "query": "query GetCountry($code: ID!) { country(code: $code) { name emoji } }",
  "variables": { "code": "JP" },
  "operationName": "GetCountry"
}

Response: {"data":{"country":{"emoji":"🇯🇵","name":"Japan"}}}

Postman

  1. New HTTP request, method POST, URL https://countries.trevorblades.com/.
  2. Body → GraphQL. Postman fetches the schema and gives autocomplete.
  3. Write the query in the QUERY box and variables in the GRAPHQL VARIABLES box. Send.

The Biggest Trap: HTTP 200 Doesn’t Mean Success

We sent three “wrong” requests to the real API. Look at the status codes:

RequestHTTP statusBody
Typo in a field name (nam)200{"errors":[{"message":"Cannot query field \"nam\" on type \"Country\". Did you mean \"name\"?", "extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
Country that doesn’t exist (ZZ)200{"data":{"country":null}}
Broken syntax (missing })400{"errors":[{"message":"The request did not contain a valid GraphQL request..."}]}
What this means for your tests A test that only checks status === 200 passes for a typo’d field and for a missing record. Every GraphQL test must assert on errors (absent, or the expected code) and on the exact data values, including null where a record doesn’t exist.

Status code behaviour varies by server (some return 400 for validation errors when using the newer “GraphQL over HTTP” conventions), so check your API’s documented behaviour and test it explicitly.

Advertisement

What to Test in a GraphQL API

AreaTest ideas
QueriesEach field returns correct values; nested objects; aliases (a: country(code:"IN") b: country(code:"JP")); fragments
VariablesRequired variable missing; wrong type (ID! given a number/null); extra variables
NullabilityNon-null fields (String!) never null; nullable fields null when data is absent
MutationsCreate/update/delete effects; returned payload; validation errors in errors or a typed error field
ErrorsError message, path, extensions.code; partial data with errors
PaginationCursor pagination (first, after, pageInfo.hasNextPage, endCursor)
AuthorizationField-level and object-level checks: a user can query me { orders } but not user(id: 2) { orders }
Performance and abuseDeeply nested queries, huge lists, aliases repeating an expensive field 100 times, batching
SchemaBreaking changes (removed fields, changed types); deprecated fields still work
IntrospectionDisabled or restricted in production if the policy says so

GraphQL Security Checks

Real-life risk: the nested query attack If author { posts { author { posts { ... } } } } can nest 20 levels, one small request can force millions of database lookups. Servers should enforce a maximum depth, a complexity/cost limit and pagination limits (e.g. first ≤ 100). Testers verify these limits return a clear error instead of timing out.
  • Introspection: { __schema { types { name } } } reveals the whole schema. Often disabled in production for internal APIs.
  • Field suggestions: “Did you mean …?” messages (like the one above) can leak hidden field names; some teams disable them in production.
  • Authorization per resolver: every field that returns sensitive data needs its own check.
  • Injection: arguments still reach databases; test them like REST inputs.
  • Batching / alias abuse: 1,000 login attempts in one request via aliases can bypass rate limiting.
Advertisement

Automating GraphQL Tests with Playwright

Playwright’s request fixture is all you need. A small helper keeps tests readable:

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

const GQL = 'https://countries.trevorblades.com/';

async function gql(request: APIRequestContext, query: string, variables?: Record<string, unknown>) {
  const res = await request.post(GQL, { data: { query, variables } });
  return { status: res.status(), body: await res.json() };
}

test('query with variables returns the right country', async ({ request }) => {
  const { status, body } = await gql(request,
    'query GetCountry($code: ID!) { country(code: $code) { name capital currency } }',
    { code: 'IN' });

  expect(status).toBe(200);
  expect(body.errors).toBeUndefined();                 // the essential GraphQL check
  expect(body.data.country).toEqual({ name: 'India', capital: 'New Delhi', currency: 'INR' });
});

test('unknown record returns null, not an error', async ({ request }) => {
  const { status, body } = await gql(request, '{ country(code: "ZZ") { name } }');
  expect(status).toBe(200);
  expect(body.errors).toBeUndefined();
  expect(body.data.country).toBeNull();
});

test('invalid field returns 200 with a validation error', async ({ request }) => {
  const { status, body } = await gql(request, '{ country(code: "IN") { nam } }');
  expect(status).toBe(200);
  expect(body.data).toBeUndefined();
  expect(body.errors[0].extensions.code).toBe('GRAPHQL_VALIDATION_FAILED');
  expect(body.errors[0].message).toContain('Cannot query field "nam"');
});

test('aliases fetch two records in one request', async ({ request }) => {
  const { body } = await gql(request, '{ in: country(code: "IN") { name } jp: country(code: "JP") { name } }');
  expect(body.data).toEqual({ in: { name: 'India' }, jp: { name: 'Japan' } });
});

These tests ran green against the live API while this lesson was written. For your own APIs, also add schema validation of data (next module) and auth tests per field.

GraphQL Tools for Testers

  • Postman / Insomnia: GraphQL body type with schema autocomplete.
  • GraphiQL / Apollo Sandbox: in-browser explorer with docs from introspection.
  • Playwright, REST Assured, pytest: GraphQL is just HTTP POST, so any API framework works.
  • GraphQL Inspector: detects breaking schema changes in CI.
  • k6: load testing GraphQL operations.
Advertisement

Mutations: Testing Writes

The countries API is read-only, so here is a typical mutation shape you’ll see in real projects:

mutation AddToCart($input: AddToCartInput!) {
  addToCart(input: $input) {
    cart { id itemCount total }
    errors { field message }         # some APIs return business errors as data
  }
}
# variables: { "input": { "productId": "101", "quantity": 2 } }

Test cases: valid input updates the cart (verify with a follow-up query); quantity 0 / -1 / 1000 returns an error in errors or the typed errors field; unknown product; another user’s cart id (authorization); running the same mutation twice (idempotency where expected).

Common Mistakes When Testing GraphQL

  • Asserting only status 200.
  • Building queries with string concatenation instead of variables.
  • Testing only the fields the UI uses; other clients may request more.
  • Ignoring null in nested data (partial failures).
  • Not testing depth/complexity limits and introspection settings.
Advertisement

Interview Questions: GraphQL Testing

How is GraphQL testing different from REST testing?

GraphQL has one endpoint and a schema; clients choose fields. Tests must check the data and errors arrays rather than relying on status codes, cover field selection, variables, nullability, field-level authorization, and abuse limits such as query depth and complexity.

Why can a GraphQL error return HTTP 200?

Because GraphQL reports errors in the response body’s errors array and can return partial data. Many servers return 200 for validation and resolver errors and use 4xx only for malformed requests.

What are queries, mutations and subscriptions?

Queries read data, mutations change data, and subscriptions push real-time updates to clients, usually over WebSocket.

What are GraphQL variables and why use them?

Values passed separately from the query text in a variables object, typed in the operation definition such as $code: ID!. They make queries reusable and prevent injection from string concatenation.

What is introspection and why might it be disabled?

A built-in feature that lets clients query the schema itself (__schema, __type). It helps tooling but exposes the full API surface, so internal APIs often disable or restrict it in production.

Advertisement
How do you test GraphQL performance risks?

Send deeply nested queries, very large page sizes, many aliases of expensive fields and batched operations, and verify the server enforces depth, complexity and pagination limits with clear errors rather than slowing down.

How does pagination usually work in GraphQL?

Through cursor-based connections: arguments like first and after, and a pageInfo object with hasNextPage and endCursor, with results in edges and node.

How do you test authorization in GraphQL?

For each sensitive type and field, call queries and mutations with different roles and users, including nested paths that reach the same data, and verify unauthorized fields return errors or null without leaking data.

Which tools do you use for GraphQL testing?

Postman or Insomnia for manual testing, GraphiQL or Apollo Sandbox for exploration, Playwright or other HTTP frameworks for automation, GraphQL Inspector for schema changes and k6 for load testing.

What is a GraphQL fragment?

A reusable set of fields defined once (fragment CountryInfo on Country { name capital }) and spread into queries with …CountryInfo, keeping large queries consistent.

Advertisement
View the full API testing syllabus