QA Interview Hub · Knowledge base · Quizzes

API

API design: resources, pagination, versioning, GraphQL

Pagination: offset vs cursor

A list isn't returned whole: thousands of records in one response kill both the server and the client. There are two main approaches.

Offset pagination: GET /transactions?limit=20&offset=40 or ?page=3&size=20. Simple to implement and you can jump to any page. The downsides: on large offsets the database still walks through the skipped rows, and if a new record was added between requests, items "shift": one appears twice and another disappears.

Cursor pagination: GET /transactions?limit=20&cursor=eyJpZCI6MTAyNH0. The server returns a nextCursor that points to the last received item, and the next page starts after it. It is stable on insertions and fast, but you can't jump to page 50.

What I check:

  • Boundaries: limit=0, negative, above the maximum, non-numeric.
  • The last page: an empty array or a missing nextCursor, not an error.
  • No duplicates and gaps when walking through all pages; the sum of items equals total, if it is given.
  • Stable sorting: without a unique key in the sort, the order between pages may float.
  • A corrupted or someone else's cursor gives 400, not 500.
In an interview: Offset is simple and lets you jump to any page, but it is slow at large offsets and skips or duplicates records on insertions. Cursor is stable and fast, and I check both variants at the boundaries, the last page and the absence of duplicates.

Filtering, sorting and versioning

Filtering and sorting usually go in the query: GET /transactions?status=completed&from=2026-01-01&sort=-createdAt. A minus before the field often means descending, but that is a convention of a specific API, so I check the documentation. I check a combination of several filters, a filter with no results (must be []), an unknown sort or filter field (400 or ignored, but documented), case and special characters in values, and date boundaries inclusive or exclusive.

Versioning is needed to change an API without breaking existing clients. Common variants:

  • In the path: /v1/accounts, /v2/accounts: the most visible and the easiest to test.
  • In a header: for example a custom Api-Version: 2 or a version in Accept.
  • In the query: ?version=2: found less often.

For QA versioning means old versions must also be regressed while they are supported. Mobile applications are especially sensitive: a user may not update the app for months, and the old client keeps going to v1. I also check that a deprecated version returns a deprecation warning if the team agreed so, and that a request without a version behaves as documented.

In an interview: I test filters and sorting in combinations, with an empty result and with unknown fields. If an API is versioned, I regress the old version too, because real clients, especially mobile ones, are still on it.

GraphQL: basics and testing

GraphQL is one endpoint (usually POST /graphql), and in the request the client describes exactly which fields it wants. A query reads data, a mutation changes it, a subscription subscribes to events. The structure is described in a schema with types.

{
  "query": "query($id: ID!) { account(id: $id) { id balance currency } }",
  "variables": { "id": "42" }
}

What is different from REST:

  • The status is often 200 even on an error; errors lie in the errors array in the body. So checking only the status in GraphQL gives almost nothing.
  • A response may be partial: part of data is there, and some fields are null with an error in errors.
  • Authorisation must be checked at the level of fields and nested objects, not only the endpoint: access to account is allowed, but isn't someone else's owner returned through a nested field.
  • Deeply nested queries and large batches can bring a server down, so I check depth and complexity limits.
  • Introspection in production is often turned off, so as not to reveal the schema.
In an interview: GraphQL has one endpoint, and the client picks the fields itself. I check not the status but data and errors, authorisation at the field level and the limits on query depth.