>_ Analyst Engineering

GraphQL for Analysts: How to Read, Query, and Test a GraphQL API

Written by Ahmed at Analyst Engineering, a Senior Technical Business Analyst with 10+ years in banking and payments delivery.

Cover for GraphQL for analysts, showing a GitHub GraphQL query for a repository's open issues and a response with data and pageInfo.

Key takeaways

  • GraphQL exposes a single endpoint where the client sends a query naming exactly the fields it wants, while REST exposes many URLs that each return a fixed shape.
  • GraphQL APIs commonly return HTTP 200 even when a query fails, with the problem described in an errors array, so a GraphQL test that asserts only the status code proves almost nothing.
  • A GraphQL response can be partial: some fields resolve to data and others to null with a matching error, so tests must assert both the absence of errors and the presence of the fields that matter.
  • Connections in the GitHub GraphQL API require a first or last argument between 1 and 100 and page with pageInfo.hasNextPage and endCursor passed back as the after argument.
  • The GraphQL schema is the contract: its types, nullability, descriptions, and deprecation markers are what an analyst reads, reviews, and derives test cases from.

GraphQL is an API style with one endpoint where the client asks for exactly the fields it needs, in a single query. For analysts it changes three habits formed on REST: the contract is a schema of types rather than a list of URLs, errors usually come back with HTTP 200, and responses can be partly successful. Test a GraphQL API like a REST API and you will pass defects straight through.

To work with a GraphQL API as an analyst: read the schema, which defines every type, field, argument, and whether a field can be null; write queries to read and mutations to change data, passing inputs as variables; send them as a POST to the single endpoint; and in every test assert on the response body, both that errors is absent when success is expected and that the fields you need are present. Page through lists with cursors rather than page numbers.

APIs for Analysts, intermediate track. Builds on Part 2, your first API collection and Part 5, API test cases. Full learning path: APIs for Analysts.

The examples use GitHub’s public GraphQL API, which needs only a personal access token, so everything below runs as written. The broader technical skills this builds on are mapped in The Technical Skills Guide for BAs.

How is GraphQL different from REST?

RESTGraphQL
EndpointsMany: /repos/{owner}/{repo}, /repos/{owner}/{repo}/issuesOne: POST /graphql
Response shapeFixed by the server per endpointChosen by the client per query
Related dataOften several callsUsually one query
Reading vs writingHTTP methods (GET, POST, PATCH)Operation type (query, mutation)
ErrorsHTTP status codesUsually HTTP 200 with an errors array
ContractOpenAPI documentGraphQL schema
VersioningOften versions (/v1, headers)Usually evolves the schema, deprecating fields
Rate limitsRequests per time windowOften query cost (GitHub uses points)

GitHub is a useful reference because it offers both. Fetching a repository and its five most recent open issues takes two REST calls; in GraphQL it is one query that returns only the fields you name.

What does a GraphQL query look like?

Here is a query against https://api.github.com/graphql:

query RepositoryOverview($owner: String!, $name: String!, $count: Int!) {
  repository(owner: $owner, name: $name) {
    nameWithOwner
    stargazerCount
    issues(states: OPEN, first: $count, orderBy: { field: CREATED_AT, direction: DESC }) {
      totalCount
      pageInfo {
        hasNextPage
        endCursor
      }
      nodes {
        number
        title
        createdAt
      }
    }
  }
}

With these variables:

{ "owner": "usebruno", "name": "bruno", "count": 5 }

Reading it as an analyst:

  • query RepositoryOverview is the operation type and a name. Names make logs and reports readable.
  • $owner: String! declares a variable; the ! means it is required.
  • repository(owner:, name:) is a field with arguments, the GraphQL equivalent of path parameters.
  • The braces list exactly the fields wanted. Nothing else comes back.
  • issues(first: $count) is a connection: a paginated list. GitHub requires first or last between 1 and 100 on connections.
  • pageInfo tells you whether more pages exist and where the next one starts.

The response mirrors the query shape exactly:

{
  "data": {
    "repository": {
      "nameWithOwner": "usebruno/bruno",
      "stargazerCount": 0,
      "issues": {
        "totalCount": 0,
        "pageInfo": { "hasNextPage": true, "endCursor": "Y3Vyc29yOnYyOp..." },
        "nodes": [
          { "number": 0, "title": "...", "createdAt": "2026-09-14T08:12:44Z" }
        ]
      }
    }
  }
}

The numbers are placeholders here; they change daily on a live repository. Notice that data sits at the top. Every GraphQL response has data, errors, or both.

How do you send a GraphQL request?

GitHub’s GraphQL API requires authentication, so create a fine-grained personal access token first, as described in Part 2.

With curl:

curl -s https://api.github.com/graphql \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query":"query { viewer { login } rateLimit { limit remaining resetAt } }"}'

Under the hood, every GraphQL request is a POST with a JSON body containing query and, optionally, variables. That is all.

In Bruno: create a request, set the method to POST and the URL to https://api.github.com/graphql, choose the GraphQL body type, paste the query into the query pane and the JSON into the variables pane, and set auth to inherit the bearer token. Bruno can fetch the schema through introspection, which gives you autocompletion and documentation for every field as you type.

In Postman: create a GraphQL request, enter the URL, set the authorization, and use the query and variables editors. Postman also introspects the schema to power autocompletion.

That rateLimit query in the curl example is worth keeping. GitHub’s GraphQL API limits by points rather than requests, 5,000 points per hour for a standard user, because a single query can ask for a lot of data. The rateLimit object shows your limit, what remains, and when it resets.

What is a mutation?

A mutation changes data. It goes to the same endpoint and returns whichever fields of the changed object you select. The GitHub schema, for example, includes an addStar mutation:

mutation StarRepository($id: ID!) {
  addStar(input: { starrableId: $id }) {
    starrable {
      stargazerCount
      viewerHasStarred
    }
  }
}

Two analyst habits apply. First, mutations take an input object, and its fields and their nullability are your requirement-level rules, exactly like a REST request schema. Second, mutations write, so run them only against your own test data or sandboxes, never against something you would not want changed. The ID comes from a previous query, which is request chaining in GraphQL form.

Why does a GraphQL error come back with HTTP 200?

Because a GraphQL request can partly succeed. Ask for a repository that does not exist and you typically get HTTP 200 with a body like this:

{
  "data": { "repository": null },
  "errors": [
    {
      "type": "NOT_FOUND",
      "path": ["repository"],
      "message": "Could not resolve to a Repository with the name 'usebruno/does-not-exist'."
    }
  ]
}

Three consequences for analysts:

  1. A status-only test is worthless. “Returns 200” passes for a query that failed completely.
  2. path locates the failure. It points at the exact field that could not resolve, which is how you tie an error to a requirement.
  3. Partial data is normal. A query for a repository and a restricted sub-field can return the repository with that sub-field as null and an error explaining why. Your requirements must say whether the consumer can use a partial result.

Non-200 codes still exist around the edges: an invalid or missing token is rejected before GraphQL runs, so authentication failures typically come back as 401. Everything past that point lives in the body.

How do you paginate in GraphQL?

With cursors. Ask for a page with first, read pageInfo, and pass endCursor back as after:

query NextIssues($owner: String!, $name: String!, $after: String) {
  repository(owner: $owner, name: $name) {
    issues(states: OPEN, first: 100, after: $after) {
      pageInfo { hasNextPage endCursor }
      nodes { number title }
    }
  }
}

A Bruno post-response script to chain pages:

const page = res.getBody().data?.repository?.issues;
if (page?.pageInfo?.hasNextPage) {
  bru.setVar("after", page.pageInfo.endCursor);
  bru.runner.setNextRequest("Next issues");
}

Cap it in real tests, as with any loop. GitHub also limits a single call to 500,000 total nodes, so deeply nested connections with large first values can be rejected outright, which is itself a test case.

How do you write tests for a GraphQL API?

Assert the body, always in two halves: no errors, and the right data.

const body = res.getBody();

test("query succeeded without errors", function () {
  expect(res.getStatus()).to.equal(200);
  expect(body.errors).to.be.undefined;
});

test("repository and open issues returned", function () {
  const repo = body.data.repository;
  expect(repo.nameWithOwner).to.equal("usebruno/bruno");
  expect(repo.issues.nodes.length).to.be.at.most(5);
  repo.issues.nodes.forEach((issue) => {
    expect(issue.number).to.be.a("number");
    expect(issue.title).to.be.a("string");
  });
});

And for a negative case, assert the error is the one you expect:

test("unknown repository returns NOT_FOUND on the repository path", function () {
  const error = res.getBody().errors?.[0];
  expect(error.type).to.equal("NOT_FOUND");
  expect(error.path).to.deep.equal(["repository"]);
  expect(res.getBody().data.repository).to.be.null;
});

The derivation method from Part 5 still applies. GraphQL adds a few sources of cases REST does not have:

GraphQL-specific caseWhat to check
Errors with 200Every invalid input produces the right error and path
Partial resultsRestricted or failing sub-fields return null plus an error, not a whole failure
NullabilityFields the schema marks non-null (!) are never null in practice
Field-level authorizationA user who can see an object cannot see its restricted fields
Query cost and depth limitsVery large or deeply nested queries are rejected cleanly, not slowly
Deprecated fieldsFields marked @deprecated still work until removal, and consumers are warned
Variables vs inline valuesThe same query behaves identically with variables and literals
Introspection policyWhether schema introspection is allowed in each environment is a deliberate security decision

How does an analyst read and review a GraphQL schema?

The schema is the contract, and it is written in a readable language:

"""A payment instruction submitted by a client."""
type Payment {
  id: ID!
  "Your reference, returned unchanged. Max 35 characters."
  endToEndId: String!
  status: PaymentStatus!
  "Settlement date. Null until the payment has settled."
  settledAt: DateTime
  legacyReference: String @deprecated(reason: "Use endToEndId.")
}

enum PaymentStatus { RCVD ACCP ACSP ACSC RJCT }

Read it the way you would read an OpenAPI file, with GraphQL’s own signals:

  • ! means non-null. settledAt without ! tells you it can be null, and the description tells you when. Every nullable field needs that “when”.
  • Descriptions are the analyst’s field. Business meaning, length rules, and lifecycle belong in those triple-quoted strings, exactly as they belong in OpenAPI descriptions, as argued in Part 4.
  • Enums are closed lists. Adding a value later can break consumers that switch on them, the classic compatibility trap covered in API versioning and breaking changes.
  • @deprecated is the change plan. GraphQL APIs usually evolve without versions, so deprecation markers are how removals are announced.

The skills for navigating a REST contract transfer directly; see reading an API contract.

The APIs for Analysts learning path

Beginner: What is an API · API glossary · JSON for analysts · HTTP status codes · Your first collection · Why did my API request fail? · Reading an API contract

Intermediate: Analyze an API · Document an API · API test cases · Chaining and scripts · Webhooks · GraphQL (you are here)

Advanced: POCs and demos · API design review · Versioning and breaking changes · API security testing · API tests in CI

The takeaway

GraphQL gives clients one endpoint and lets them ask for exactly the fields they need, with queries to read, mutations to write, and variables for inputs. For analysts, the schema is the contract, with non-null markers, descriptions, enums, and deprecations to review. The biggest testing shift is that errors usually arrive with HTTP 200 and results can be partial, so every test asserts the body: no errors when success is expected, the right path and message when it is not, and the fields that matter actually present. Page with cursors, cap your loops, and watch query cost limits.

For the wider technical path, see The Technical Skills Guide for BAs, and for test design that carries across REST and GraphQL, API Testing and QA Mastery for BAs. Moving onto a GraphQL project? A 1:1 Tech BA Coaching Call is a fast way to get oriented.

Ahmed is a Senior Technical Business Analyst with 10+ years in banking and payments. He builds practical guides and tools for analysts at The Tech BA Toolkit.

Tags: GraphQL, API Testing, Bruno, Postman, Technical Analyst

About the author

Analyst Engineering is written by Ahmed, a Senior Technical Business Analyst with 10+ years of banking and payments delivery experience: ISO 20022 and SWIFT messaging, payments API integration, Kafka event validation, and production support. Every article comes from real delivery work, and each one is reviewed and updated as tools and standards change.

Free account

Practice on the Labs, keep your progress

A free account, no password: an email link signs you in. It saves your steps and self-assessments on the Labs, shows your missions on a dashboard, unlocks the solutions, and, if you tick the box, sends you new missions and articles when they ship.

Your email is used to sign you in. Nothing else, unless you ask. Privacy.