GraphQL vs REST — Key Differences

8 min read

GraphQL is the second most common API style after REST, and for many modern apps — especially mobile and dashboard-heavy ones — it's the primary style. As a QA engineer you'll meet plenty of teams running both: a public REST API for partners, an internal GraphQL endpoint for the web and mobile clients. The testing principles you've learned still apply (auth, validation, error handling, performance), but the mechanics change. This lesson sets up the differences so the next three lessons can dive into the specifics.

What GraphQL actually is

GraphQL is a query language for APIs. The client writes a query specifying exactly what data it wants, and the server returns exactly that — no more, no less.

Where a REST API exposes many endpoints — /users, /users/123, /users/123/orders, /products — a GraphQL API exposes a single endpoint, usually /graphql. Every read, every write, every subscription goes through that one URL. The shape of the request body decides what happens.

A typical GraphQL query:

query {
  user(id: 42) {
    name
    email
    orders {
      id
      total
    }
  }
}

And a response that mirrors the shape exactly:

{
  "data": {
    "user": {
      "name": "Alice",
      "email": "alice@test.com",
      "orders": [
        { "id": 9001, "total": 49.99 },
        { "id": 9002, "total": 12.50 }
      ]
    }
  }
}

That mirroring — request shape equals response shape — is GraphQL's defining feature. There's no fixed /users/123 payload to consume; the client describes the payload it wants.

The headline differences

REST vs GraphQL at a glance

REST

  • Many endpoints

    /users, /products, /orders — one URL per resource type.

  • Server defines response shape

    GET /users/123 returns whatever the server chose to include.

  • Verbs everywhere

    GET, POST, PUT, PATCH, DELETE map to operations.

  • Versioned via URL

    /v1/, /v2/ — old clients keep working until the version is retired.

  • HTTP-native error codes

    404, 401, 500 distinguish failures.

  • Easier to cache

    Standard HTTP caching applies — CDNs work out of the box.

GraphQL

  • Single /graphql endpoint

    All operations POST to one URL with a query in the body.

  • Client picks fields

    The query declares exactly which fields the response should include.

  • Three operation types

    Query (read), mutation (write), subscription (live updates).

  • Schema evolves, no versions

    Add fields freely; mark deprecated ones; removals require migration.

  • Errors in the response body

    HTTP 200 even when the operation failed; check the errors array.

  • Caching is harder

    Every query is a unique POST body — CDNs need GraphQL-aware tooling.

The implications run deep. With REST you test endpoints; with GraphQL you test queries. With REST a 404 tells you the resource is missing; with GraphQL a 200 with {"errors": [...]} does. The conceptual map shifts, even though the underlying concerns — auth, performance, correctness — don't.

Over-fetching and under-fetching

REST's fixed payloads mean clients often over-fetch — receiving fields they don't need — or under-fetch — needing multiple requests to assemble what they want.

A user profile page that needs the user, their orders, and the order items would, in REST, look like:

  1. GET /users/123 — user object.
  2. GET /users/123/orders — list of orders.
  3. GET /orders/9001/items, GET /orders/9002/items, … — items per order.

Three round trips minimum, more on a typical mobile network. In GraphQL, the same data is one query:

query {
  user(id: 123) {
    name
    orders {
      id
      total
      items {
        productId
        quantity
      }
    }
  }
}

One round trip. The mobile-bandwidth case is one of the strongest arguments for GraphQL.

When each style wins

GraphQL shines when:

  • The client controls the data shape (mobile, single-page web apps).
  • Many small relationships need to be traversed (social graphs, e-commerce catalogues).
  • Bandwidth or round-trip count matters (mobile, slow networks).
  • A single team owns both client and server (the schema can evolve fast).

REST shines when:

  • Public consumers of all kinds need to plug in (third-party integrators).
  • HTTP caching is important (CDNs, browser caching).
  • Operations map cleanly to CRUD on resources (admin tools, simple data APIs).
  • Tooling familiarity matters (every dev knows REST; GraphQL has a learning curve).

Most large companies run both. As QA, that's your reality — you'll need fluency in both styles.

What changes for testing

Conceptually, very little. You still test:

  • Functional correctness — does the right data come back?
  • Validation — does the API reject bad inputs?
  • Auth — are tokens enforced on every operation?
  • Error handling — does the API fail gracefully?
  • Performance — does it respond within budget?
  • Schema — does the response match the contract?

Mechanically, several things shift:

  • HTTP method: every GraphQL request is a POST to /graphql, even reads. Asserting on method is rarely useful.
  • Status codes: HTTP 200 is normal for every response — including errors. The errors live in the body.
  • Single endpoint, many operations: you can no longer talk about "testing the /users endpoint" — you test queries that return users, mutations that modify users, and so on.
  • Schema as contract: the server publishes a typed schema (introspection or SDL files). Schema-aware testing tools can auto-generate tests.
  • Tooling: GraphQL Playground, Apollo Studio, Insomnia, and Postman all have GraphQL-specific UIs that make writing test queries easier than hand-building JSON.

A first taste of testing GraphQL

Sending a query with curl:

curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJI..." \
  -d '{"query": "{ user(id: 42) { name email } }"}'

The body is JSON with a query field containing the GraphQL query string. That's it. The response will be JSON with data, errors, or both.

A test for the same query, in pseudo-Python:

response = requests.post(
    "https://api.example.com/graphql",
    json={"query": "{ user(id: 42) { name email } }"},
    headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
body = response.json()
assert body.get("errors") is None, f"GraphQL errors: {body['errors']}"
assert body["data"]["user"]["email"] == "alice@test.com"

Three assertions: HTTP status is 200, no errors in the body, expected data returned. That trio is the foundation of every GraphQL test.

⚠️ Common mistakes

  • Treating HTTP status as the source of truth. A 200 with errors populated is still a failure. GraphQL tests must check errors explicitly.
  • Carrying over REST URL intuition. "Where's the user endpoint?" misses the point. There's one endpoint; the query describes what you want.
  • Assuming GraphQL replaces REST. Many teams run both, with REST for stable public APIs and GraphQL for client-shaped reads. Your test suite needs to handle both.

🎯 Practice task

Compare REST and GraphQL on the same data. 30 minutes.

  1. Open the GitHub REST API and GitHub GraphQL API docs side by side. They expose much of the same data.
  2. Pick a use case: "fetch the last 5 issues for a repository, with the author's name and a count of comments per issue."
  3. Sketch the REST flow. How many calls? Which endpoints?
  4. Write the GraphQL query that does it in one request. Use the GitHub GraphQL Explorer to run it.
  5. Compare the responses: payload size, number of round trips, fields you didn't ask for in REST.
  6. Stretch: add a hypothetical "for each issue, also fetch the latest comment's body and timestamp." Re-do both flows. The REST count will balloon; the GraphQL query stays one request.

You now know what GraphQL is and how it differs from REST. The next lesson covers the three operation types you'll meet — queries, mutations, and subscriptions — in concrete detail.

// tip to track lessons you complete and pick up where you left off across devices.