API testing
Verify that an API honors its contract, enforces its authorization rules, and fails correctly, without depending on a browser or a full end-to-end environment.
When to invoke
- "Write integration tests for these endpoints."
- "Validate responses against our OpenAPI spec."
- "Test authentication and authorization on this API."
- "Our API returns 200 with an error body; how do we test that?"
- "Add API tests to CI."
Test the contract, then the behavior
| Layer |
Question |
Typical assertion |
| Transport |
Did the call succeed at HTTP level? |
Status code, headers, content type |
| Schema |
Does the payload match the declared shape? |
Validate against OpenAPI or GraphQL schema |
| Semantics |
Is the value correct for this input? |
Field values, computed results |
| Authorization |
Can the wrong caller reach it? |
401 without token, 403 with wrong role |
| Side effects |
Did state change as promised? |
Re-read, or verify emitted event |
Testing only the happy path with a 200 assertion is the most common and least useful API suite.
Validate against the schema, not hand-written shapes
Hand-written expected payloads drift from the spec. Assert against the source of truth.
// Validate the response against the OpenAPI schema itself
const valid = ajv.validate(schema.components.schemas.User, response.body);
expect(valid, JSON.stringify(ajv.errors)).toBe(true);
For GraphQL, run introspection or load the SDL and validate both the query and the response. A query that requests a removed field must fail the build, not silently return null.
Authorization is a test matrix, not a single case
Every protected endpoint needs coverage for each meaningful caller:
- No credentials — expect 401.
- Valid credentials, wrong role — expect 403.
- Valid credentials, correct role, another tenant's resource — expect 403 or 404, and be deliberate about which.
- Expired or malformed token — expect 401, never 500.
The cross-tenant case is where real breaches occur and where suites are usually silent.
Error paths deserve explicit assertions
- Assert the status code and the error body shape, including a stable machine-readable code.
- Confirm the error does not leak stack traces, SQL, internal hostnames, or credentials.
- Verify validation errors identify the offending field.
- Check that an unexpected server error returns 5xx rather than a 200 with an error payload.
Pagination, ordering, and idempotency
- Pagination: assert page size, that cursors advance, and that the final page terminates. Verify no duplicates or gaps across pages under a stable sort.
- Ordering: if order is part of the contract, assert it; if not, do not assert it, or the test becomes flaky.
- Idempotency: replay PUT and DELETE and confirm the same terminal state. For POST with an idempotency key, replay must not create a second resource.
Test data and isolation
- Create the data each test needs and clean it up, or use a transaction rolled back per test.
- Never depend on records that another test created; ordering dependencies cause intermittent failures.
- Parameterize identifiers so parallel runs do not collide.
- Keep credentials in the environment or a secret store, never in the repository or in assertions.
Gotchas
- Asserting the whole response body breaks on every additive field. Assert the fields under test plus schema validity.
- Time and timezone leak into assertions. Freeze or inject clocks rather than comparing to
now.
- Retries can hide flakiness and non-idempotent bugs. If a retry makes it pass, investigate before adding the retry.
- Recorded fixtures go stale silently. Re-record on a schedule or verify against a live contract.
- Rate limits fail CI unpredictably. Use a dedicated test tenant or account with known limits.
Output template
## API test result
**Status:** pass | fail | blocked
**Summary:** <endpoints covered and outcome>
### Details
| Endpoint | Case | Expected | Result |
| --- | --- | --- | --- |
| <method path> | <happy, auth, error, pagination> | <status and shape> | <pass or fail> |
Schema source: <OpenAPI or GraphQL SDL used for validation>
Authorization matrix covered: <cases exercised>
### Validation
- Schema validation performed: <checked and result>
- Cross-tenant access case covered: <checked and result>
Quality gate
References
1---2name: api-testing-23description: Test REST, GraphQL, and gRPC APIs at the contract and behavior level with schema validation, authentication and authorization coverage, error-path assertions, pagination and idempotency checks, and CI-ready suites. Use when the user asks to test an API, validate responses against OpenAPI or a GraphQL schema, cover auth and error cases, or build an integration suite for endpoints.4license: MIT5---67# API testing89Verify that an API honors its contract, enforces its authorization rules, and fails correctly, without depending on a browser or a full end-to-end environment.1011## When to invoke1213- "Write integration tests for these endpoints."14- "Validate responses against our OpenAPI spec."15- "Test authentication and authorization on this API."16- "Our API returns 200 with an error body; how do we test that?"17- "Add API tests to CI."1819## Test the contract, then the behavior2021| Layer | Question | Typical assertion |22| --- | --- | --- |23| Transport | Did the call succeed at HTTP level? | Status code, headers, content type |24| Schema | Does the payload match the declared shape? | Validate against OpenAPI or GraphQL schema |25| Semantics | Is the value correct for this input? | Field values, computed results |26| Authorization | Can the wrong caller reach it? | 401 without token, 403 with wrong role |27| Side effects | Did state change as promised? | Re-read, or verify emitted event |2829Testing only the happy path with a 200 assertion is the most common and least useful API suite.3031## Validate against the schema, not hand-written shapes3233Hand-written expected payloads drift from the spec. Assert against the source of truth.3435```javascript36// Validate the response against the OpenAPI schema itself37const valid = ajv.validate(schema.components.schemas.User, response.body);38expect(valid, JSON.stringify(ajv.errors)).toBe(true);39```4041For GraphQL, run introspection or load the SDL and validate both the query and the response. A query that requests a removed field must fail the build, not silently return null.4243## Authorization is a test matrix, not a single case4445Every protected endpoint needs coverage for each meaningful caller:4647- **No credentials** — expect 401.48- **Valid credentials, wrong role** — expect 403.49- **Valid credentials, correct role, another tenant's resource** — expect 403 or 404, and be deliberate about which.50- **Expired or malformed token** — expect 401, never 500.5152The cross-tenant case is where real breaches occur and where suites are usually silent.5354## Error paths deserve explicit assertions5556- Assert the **status code and the error body shape**, including a stable machine-readable code.57- Confirm the error **does not leak** stack traces, SQL, internal hostnames, or credentials.58- Verify **validation errors identify the offending field**.59- Check that an unexpected server error returns 5xx rather than a 200 with an error payload.6061## Pagination, ordering, and idempotency6263- **Pagination:** assert page size, that cursors advance, and that the final page terminates. Verify no duplicates or gaps across pages under a stable sort.64- **Ordering:** if order is part of the contract, assert it; if not, do not assert it, or the test becomes flaky.65- **Idempotency:** replay PUT and DELETE and confirm the same terminal state. For POST with an idempotency key, replay must not create a second resource.6667## Test data and isolation6869- Create the data each test needs and clean it up, or use a transaction rolled back per test.70- Never depend on records that another test created; ordering dependencies cause intermittent failures.71- Parameterize identifiers so parallel runs do not collide.72- Keep credentials in the environment or a secret store, never in the repository or in assertions.7374## Gotchas7576- **Asserting the whole response body** breaks on every additive field. Assert the fields under test plus schema validity.77- **Time and timezone leak into assertions.** Freeze or inject clocks rather than comparing to `now`.78- **Retries can hide flakiness and non-idempotent bugs.** If a retry makes it pass, investigate before adding the retry.79- **Recorded fixtures go stale silently.** Re-record on a schedule or verify against a live contract.80- **Rate limits fail CI unpredictably.** Use a dedicated test tenant or account with known limits.8182## Output template8384```markdown85## API test result8687**Status:** pass | fail | blocked88**Summary:** <endpoints covered and outcome>8990### Details91| Endpoint | Case | Expected | Result |92| --- | --- | --- | --- |93| <method path> | <happy, auth, error, pagination> | <status and shape> | <pass or fail> |9495Schema source: <OpenAPI or GraphQL SDL used for validation>96Authorization matrix covered: <cases exercised>9798### Validation99- Schema validation performed: <checked and result>100- Cross-tenant access case covered: <checked and result>101```102103## Quality gate104105- [ ] Responses are validated against the declared schema, not hand-written shapes.106- [ ] Every protected endpoint covers unauthenticated, wrong-role, and cross-tenant cases.107- [ ] Error paths assert status, stable error code, and absence of sensitive leakage.108- [ ] Pagination assertions cover advance and termination without duplicates or gaps.109- [ ] Ordering is asserted only when it is part of the contract.110- [ ] Tests create and clean their own data and can run in parallel.111- [ ] No credentials appear in the repository, logs, or assertions.112- [ ] Retries are not used to mask non-deterministic behavior.113114## References115116- [OpenAPI Specification](https://spec.openapis.org/oas/latest.html)117- [GraphQL specification](https://spec.graphql.org/)118- [JSON Schema](https://json-schema.org/specification)119- [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html)