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.
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?
| REST | GraphQL | |
|---|---|---|
| Endpoints | Many: /repos/{owner}/{repo}, /repos/{owner}/{repo}/issues | One: POST /graphql |
| Response shape | Fixed by the server per endpoint | Chosen by the client per query |
| Related data | Often several calls | Usually one query |
| Reading vs writing | HTTP methods (GET, POST, PATCH) | Operation type (query, mutation) |
| Errors | HTTP status codes | Usually HTTP 200 with an errors array |
| Contract | OpenAPI document | GraphQL schema |
| Versioning | Often versions (/v1, headers) | Usually evolves the schema, deprecating fields |
| Rate limits | Requests per time window | Often 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 RepositoryOverviewis 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 requiresfirstorlastbetween 1 and 100 on connections.pageInfotells 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:
- A status-only test is worthless. “Returns 200” passes for a query that failed completely.
pathlocates the failure. It points at the exact field that could not resolve, which is how you tie an error to a requirement.- Partial data is normal. A query for a repository and a restricted sub-field can return the repository with that sub-field as
nulland 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 case | What to check |
|---|---|
| Errors with 200 | Every invalid input produces the right error and path |
| Partial results | Restricted or failing sub-fields return null plus an error, not a whole failure |
| Nullability | Fields the schema marks non-null (!) are never null in practice |
| Field-level authorization | A user who can see an object cannot see its restricted fields |
| Query cost and depth limits | Very large or deeply nested queries are rejected cleanly, not slowly |
| Deprecated fields | Fields marked @deprecated still work until removal, and consumers are warned |
| Variables vs inline values | The same query behaves identically with variables and literals |
| Introspection policy | Whether 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.settledAtwithout!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.
@deprecatedis 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.
Related articles
- What Is an API? How APIs Actually Work, Explained for Analysts What an API is and how one works, for analysts: request and response, methods, headers, auth, status codes, and a real GitHub API call you can send today.
- How to Write API Test Cases: 40 Tests Derived From One Endpoint How to write API test cases from the contract: a six-source derivation method, 40 worked cases for one payment endpoint, and data-driven automation in Bruno.
- Chaining API Requests With JavaScript in Bruno and Postman: The Scripts Analysts Need Chain API requests in Bruno and Postman: capture values, pre-request and post-response scripts, token refresh, polling, branching, and a Stripe sandbox flow.
- Reading an API Contract: OpenAPI Without a Developer How an analyst reads an API contract: endpoints, methods, request and response schemas, status codes, and OpenAPI structure. Understand any API without asking a developer.
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.