API Testing Practice
The practice of testing an API rather than the HTTP basics — validating responses against a schema, error contracts, pagination and filtering, rate limits, GraphQL, file transfer, and backward-compatible versioning.
9 questions
JuniorTheoryVery commonBeyond the happy path, what does an API's error contract cover?
Beyond the happy path, what does an API's error contract cover?
The happy path is one valid case; the error contract is how the API behaves on everything else — bad input, missing auth, absent resources, conflicts. It specifies the right status code per class (400 malformed, 401/403 auth, 404 absent, 409 conflict, 422 valid-but-rejected) plus a stable, parseable error body. Testing it means driving each class deliberately and asserting the code and message, not only that a 200 comes back.
Common mistakes
- ✗Only asserting the 200 happy path and never exercising the failure classes
- ✗Accepting any 4xx as fine instead of the specific code the class demands
- ✗Ignoring the shape of the error body, so clients cannot parse it reliably
Follow-up questions
- →When would you return 422 rather than 400 for a request the server rejects?
- →Why should an error body stay stable across releases just like a success body?
JuniorTheoryCommonWhat is schema validation of an API response, and what does it catch?
What is schema validation of an API response, and what does it catch?
Schema validation asserts the shape of a response — field names, data types, required fields, enums, formats — against a machine-readable contract such as JSON Schema or an OpenAPI spec. A single assertion covers the whole payload and catches type drift and missing or renamed fields that hand-picked field checks walk straight past. It proves shape, not that the values are business-correct.
Common mistakes
- ✗Asserting a handful of fields by hand instead of validating the whole payload
- ✗Believing a passing schema also proves the values are business-correct
- ✗Leaving required and additionalProperties unset, so a missing or extra field still passes
Follow-up questions
- →What does a schema with additionalProperties set to false catch that a lenient one misses?
- →How do you keep the schema used by your tests in sync with the service OpenAPI spec?
MiddleTheoryCommonHow does testing the API styles GraphQL and REST differ for a tester?
How does testing the API styles GraphQL and REST differ for a tester?
GraphQL exposes one endpoint where the client shapes the query, so testing moves from per-endpoint status codes to per-query behaviour: over- and under-fetching, nested resolution, and partial results. A GraphQL response is typically 200 even on failure, carrying errors in an errors array — you assert on that array, not the HTTP code. The typed GraphQL schema is the contract you validate against. REST testing instead leans on distinct URLs, verbs, and status codes per resource.
Common mistakes
- ✗Asserting the HTTP status instead of the errors array on a GraphQL response
- ✗Assuming a REST endpoint-per-resource model maps onto GraphQL's single endpoint
- ✗Skipping over- and under-fetching checks because the client shapes the query
Follow-up questions
- →How do you test that a GraphQL query returns partial data plus errors together?
- →Why can you not rely on the HTTP status code alone to judge a GraphQL call?
MiddleDesignCommonA GET /orders list endpoint takes page and pageSize (default 20, max 100), a status filter, and a sort parameter (createdAt or total, asc or desc). It returns items plus a total count. Product wants confidence that paging, filtering and sorting all behave under real data. Design the test approach: which cases you cover for pagination boundaries, how you prove the filter and the sort are actually applied rather than ignored, how paging and filtering interact, and what you assert about the total count and page stability while rows are being inserted. Note the response-shape check you rely on across every case.
A GET /orders list endpoint takes page and pageSize (default 20, max 100), a status filter, and a sort parameter (createdAt or total, asc or desc). It returns items plus a total count. Product wants confidence that paging, filtering and sorting all behave under real data. Design the test approach: which cases you cover for pagination boundaries, how you prove the filter and the sort are actually applied rather than ignored, how paging and filtering interact, and what you assert about the total count and page stability while rows are being inserted. Note the response-shape check you rely on across every case.
Cover pagination boundaries — first, last, empty, past-the-end, and pageSize at 1, at max, over max (clamped or rejected). Prove sorting by asserting the returned order for each key and direction, ties included. Prove filtering: every row matches the filter and a known non-match is absent; combine filter with paging so counts stay consistent. Assert total reflects the filter, not the page; order by a stable key so pages do not drift. Schema-validate each response.
Common mistakes
- ✗Testing each parameter alone and never the filter-plus-paging interaction
- ✗Treating total as the page length instead of the full filtered result size
- ✗Sorting by an unstable key, so pages drift or overlap as rows are inserted
Follow-up questions
- →How would you detect that page 2 dropped or repeated a row after new inserts?
- →What edge case do you expect when pageSize is requested above its documented max?
SeniorDesignCommonYour automated API suite must hit endpoints protected by the authorization framework OAuth2, where a bearer token is fetched from a token endpoint and expires after an hour. Design how you automate the auth so the suite stays fast, stable and secure: how you acquire and reuse a token across many tests instead of authenticating on every request, how the suite detects an expired token and recovers mid-run, where the client credentials live so nothing secret is committed, and which scope the test client should hold. Include the negative cases you assert — a missing token, an expired token, and a token whose scope is insufficient — and say how you keep a protected response verified against its schema.
Your automated API suite must hit endpoints protected by the authorization framework OAuth2, where a bearer token is fetched from a token endpoint and expires after an hour. Design how you automate the auth so the suite stays fast, stable and secure: how you acquire and reuse a token across many tests instead of authenticating on every request, how the suite detects an expired token and recovers mid-run, where the client credentials live so nothing secret is committed, and which scope the test client should hold. Include the negative cases you assert — a missing token, an expired token, and a token whose scope is insufficient — and say how you keep a protected response verified against its schema.
Fetch a token once via the token endpoint and cache it across tests instead of authenticating per request. Detect expiry from the exp claim or a 401, refresh proactively or reactively on the 401, then retry the original call. Keep client id and secret in secure config or env, never committed, and give the test client the least scope it needs. Assert the negatives — 401 on a missing or expired token, 403 on insufficient scope — and schema-validate the protected responses.
Common mistakes
- ✗Running the full auth flow per test instead of caching one token across the run
- ✗Hardcoding client credentials in test files instead of secure config or env
- ✗Skipping the missing / expired / insufficient-scope negative assertions
Follow-up questions
- →How does the suite recover when a cached token expires halfway through a run?
- →Why give the test client the least scope rather than a broad administrative one?
MiddleDesignOccasionalA POST /documents endpoint accepts a multipart file up to 10 MB, allowing only PDF and PNG, and returns metadata (id, size, contentType). GET /documents/{id} streams the file back. Design the test approach for this pair: which upload cases you cover around the size limit and the allowed types, how you prove a downloaded file is byte-for-byte the one you uploaded, which response headers you assert on the download, and which malicious or malformed inputs you deliberately try. Note how you handle a large file without the test itself running out of memory, and the response-shape check you apply to the upload result.
A POST /documents endpoint accepts a multipart file up to 10 MB, allowing only PDF and PNG, and returns metadata (id, size, contentType). GET /documents/{id} streams the file back. Design the test approach for this pair: which upload cases you cover around the size limit and the allowed types, how you prove a downloaded file is byte-for-byte the one you uploaded, which response headers you assert on the download, and which malicious or malformed inputs you deliberately try. Note how you handle a large file without the test itself running out of memory, and the response-shape check you apply to the upload result.
Upload: test the size boundary (10 MB passes, over is rejected), allowed versus disallowed types, an empty and a truncated file, and an extension-versus-MIME mismatch. Download: prove the bytes round-trip identically via a checksum, and assert Content-Type, Content-Disposition and status. Try malicious inputs — an executable renamed to .pdf, path traversal in the filename. Stream large files rather than buffering them whole so the test stays within memory. Schema-validate the upload metadata.
Common mistakes
- ✗Testing only a small valid upload and skipping the size and type boundaries
- ✗Never comparing downloaded bytes to the original, so silent corruption slips through
- ✗Treating disguised-executable and traversal inputs as out of scope for functional QA
Follow-up questions
- →How do you detect that a downloaded file was silently truncated or corrupted?
- →Why is checking the real MIME type safer than trusting the uploaded file extension?
MiddleDesignOccasionalAn API caps each key at 100 requests per minute and answers 429 with a Retry-After header once the cap is hit. Product also complains that legitimate traffic bursts sometimes get throttled. Design how you test the rate limiter: how you confirm the cap triggers at the right count, what you assert on the throttled response, how you check the window resets, and how you separate a genuine limiter defect from a false positive that punishes an honest burst. Say how you keep the test itself from being flaky given that timing and a shared counter are involved, and note what you would measure to tell whether the limit is set too aggressively for real usage.
An API caps each key at 100 requests per minute and answers 429 with a Retry-After header once the cap is hit. Product also complains that legitimate traffic bursts sometimes get throttled. Design how you test the rate limiter: how you confirm the cap triggers at the right count, what you assert on the throttled response, how you check the window resets, and how you separate a genuine limiter defect from a false positive that punishes an honest burst. Say how you keep the test itself from being flaky given that timing and a shared counter are involved, and note what you would measure to tell whether the limit is set too aggressively for real usage.
Drive requests to the boundary: request 100 succeeds and 101 returns 429 with a Retry-After the client can honour. Confirm the window resets — after Retry-After, calls succeed again. Separate a real defect from a false positive by modelling the documented burst, which must not be throttled; only genuine over-limit traffic is. Keep the test stable with a dedicated key and controlled clock, never a shared counter. To judge if the cap is too tight, measure the 429 rate against real traffic.
Common mistakes
- ✗Accepting any 429 as a pass instead of pinning the exact trigger count
- ✗Never asserting Retry-After or that the window actually resets afterwards
- ✗Assuming every throttle is correct, so false positives on honest bursts go unnoticed
Follow-up questions
- →How do you keep a timing-dependent rate-limit test from turning flaky in CI?
- →What metric tells you the limit is throttling legitimate users, not just abusers?
MiddleDesignOccasionalYour team is shipping /v2 of an API while /v1 stays live for existing mobile clients that update slowly. In /v2 a field was renamed and a previously optional field became required. Design how you test that the change is backward-compatible: how you confirm the still-live /v1 behaves exactly as before for old clients, how you decide whether a given change is additive or breaking, how you exercise both versions in parallel without one leaking into the other, and how you catch a breaking change before it reaches production. Say what a consumer-driven contract test buys you here, and how you would verify a version is being deprecated cleanly rather than pulled out from under its callers.
Your team is shipping /v2 of an API while /v1 stays live for existing mobile clients that update slowly. In /v2 a field was renamed and a previously optional field became required. Design how you test that the change is backward-compatible: how you confirm the still-live /v1 behaves exactly as before for old clients, how you decide whether a given change is additive or breaking, how you exercise both versions in parallel without one leaking into the other, and how you catch a breaking change before it reaches production. Say what a consumer-driven contract test buys you here, and how you would verify a version is being deprecated cleanly rather than pulled out from under its callers.
Pin v1: run its contract tests unchanged so old clients still see the same fields, types and status codes. Classify each change — a new optional field or endpoint is additive; a renamed or removed field, a newly-required input, or a changed type or status is breaking and must not land in a live version. Test v1 and v2 side by side so a v1 call never gets v2 semantics. Use consumer-driven contract tests so a breaking change fails CI before release.
Common mistakes
- ✗Not re-running the v1 contract tests once v2 ships, so v1 regressions go unseen
- ✗Treating a renamed field or newly-required input as a safe in-place change
- ✗Testing only the newest version and never the two side by side
Follow-up questions
- →Which specific change types would you class as breaking versus additive, and why?
- →How does a consumer-driven contract test fail before a breaking change reaches prod?
SeniorDesignOccasionalA payment API exposes PUT /accounts/{id} to update an account and DELETE /accounts/{id} to close it, and both are documented as idempotent so a client can safely retry after a network timeout. Design how you verify that idempotency claim rather than just the happy path: what you assert when the same PUT is sent several times, what should happen on a second DELETE of an already-removed account, how you prove a retry never fires a side effect twice (a double charge or a duplicate record), and how you reproduce the real trigger — a client resending after a timeout. Say which status code you expect per repeat given the documented contract, and what you validate about the resource state after the repeats settle.
A payment API exposes PUT /accounts/{id} to update an account and DELETE /accounts/{id} to close it, and both are documented as idempotent so a client can safely retry after a network timeout. Design how you verify that idempotency claim rather than just the happy path: what you assert when the same PUT is sent several times, what should happen on a second DELETE of an already-removed account, how you prove a retry never fires a side effect twice (a double charge or a duplicate record), and how you reproduce the real trigger — a client resending after a timeout. Say which status code you expect per repeat given the documented contract, and what you validate about the resource state after the repeats settle.
Repeat each call and compare final state, not just the first response. A PUT sent N times leaves the resource in the same state each time, with equivalent responses. The first DELETE removes it (200/204); a repeat on the absent account returns the documented status (404 or 204) with no extra side effect. Prove repeats never duplicate a charge, row, or side effect, via downstream state. Reproduce a client resend after a timeout and schema-validate the result.
Common mistakes
- ✗Asserting only response equality, never the final resource state after repeats
- ✗Expecting a second DELETE to error out instead of the documented idempotent status
- ✗Never reproducing the real trigger — a client resend after a timeout
Follow-up questions
- →Should a second DELETE return 404 or 204, and how do you decide which is correct?
- →How would you detect that a retried PUT silently fired its side effect twice?