Lesson 2 of 9 · API Testing
REST APIs Explained
Resources, URIs, CRUD mapping, statelessness, pagination, filtering and versioning: how REST APIs are designed and what that means for the tests you write.
beginner 9 min read Updated
You will learn to
- Describe the REST architectural constraints in practical terms
- Map CRUD operations to HTTP methods, URIs and expected status codes
- Design positive, negative and edge-case tests for a REST resource
- Recognise pagination, filtering and versioning patterns and how to test them
Prerequisites
- HTTP Fundamentals
Most APIs you will test at work describe themselves as REST APIs. Knowing what that actually means tells you what behaviour to expect before you read a line of documentation: which URL holds which data, which method changes it, and which status code should come back. It also gives you a vocabulary for bug reports: “this endpoint returns 200 with an error body instead of 404” is a precise, fixable report.
This lesson assumes you are comfortable with HTTP Fundamentals.
What REST is
REST (Representational State Transfer) is an architectural style described by Roy Fielding in his 2000 doctoral dissertation. It is not a protocol or a standard with a specification you can validate against. It is a set of constraints, and in practice “REST API” usually means “a resource-oriented API over HTTP that uses methods and status codes as intended”.
The constraints, translated into what they mean for testers:
| Constraint | Meaning | What it implies for testing |
|---|---|---|
| Client–server | UI and data storage are separated | You can test the API independently of any frontend |
| Stateless | Each request contains everything needed to process it | Every request must carry auth; test that a request works in isolation |
| Cacheable | Responses say whether they can be cached | Check Cache-Control and ETag behaviour |
| Uniform interface | Resources identified by URIs, manipulated through representations, with standard methods | Predictable URL and method conventions you can check consistently |
| Layered system | Proxies, gateways and load balancers can sit in between | Gateways can produce their own 502, 503, 504 and 429 responses |
| Code on demand (optional) | Server can send executable code | Rarely relevant to API testing |
The uniform interface also includes HATEOAS (hypermedia as the engine of application state): responses contain links to related actions. Few production APIs implement it fully, so do not expect it unless the documentation says so.
Resources and URIs
In REST, everything is a resource: a user, an order, a product, a list of a user’s orders. Each resource has a URI. Conventions you will see in well-designed APIs:
/users collection of users
/users/42 one user
/users/42/orders orders belonging to user 42
/users/42/orders/1001 one order of user 42
/orders?status=paid collection filtered by a query parameter
- Nouns, not verbs:
/users, not/getUsers. The method is the verb. - Plural collection names:
/users/42rather than/user/42. - Nesting expresses ownership, but deep nesting (more than two levels) is usually a design smell.
- Query parameters filter, sort and paginate collections; they do not identify a single resource.
These are conventions, not rules. Your job is to check that the API is consistent with its own documentation and with itself. An API that uses /users/42 but /order/1001 is a usability issue worth raising.
CRUD mapped to HTTP
The core of a REST resource is the four CRUD operations. Here is the conventional mapping with the status codes a tester should expect:
| Operation | Method and URI | Success status | Typical body |
|---|---|---|---|
| Create | POST /users |
201 Created + Location header |
The created user |
| Read one | GET /users/42 |
200 OK |
The user |
| Read many | GET /users |
200 OK |
Array or paginated wrapper (empty array if none) |
| Replace | PUT /users/42 |
200 OK or 204 No Content |
Updated user, or nothing |
| Partial update | PATCH /users/42 |
200 OK or 204 No Content |
Updated user, or nothing |
| Delete | DELETE /users/42 |
204 No Content (or 200/202) |
Usually empty |
And the common failure statuses for the same resource:
| Situation | Expected status |
|---|---|
GET, PUT, PATCH or DELETE on a non-existent id |
404 Not Found |
| Create with a duplicate unique field (e.g. email) | 409 Conflict |
| Invalid field values | 400 Bad Request or 422 Unprocessable Content (check the docs) |
| No or invalid credentials | 401 Unauthorized |
| Valid credentials, insufficient permission | 403 Forbidden |
| Unsupported method on the URI | 405 Method Not Allowed |
PUT vs PATCH
PUT replaces the resource with the representation you send. Fields you omit should be reset or rejected, depending on the API. PATCH modifies only the fields you send. A classic test:
PATCH /users/42 HTTP/1.1
Content-Type: application/json
{"name":"Ana Souza"}
Then GET /users/42 and check that name changed and every other field is unchanged. Repeat with PUT and the same partial body, and check the documented behaviour: either the omitted fields are cleared, or you get a 400/422 asking for a complete representation. A PUT that behaves like a PATCH is a common, subtle inconsistency.
Statelessness in practice
Because each request is independent, a REST API should not depend on the order of previous calls in the same “session” beyond the data they created. Tests that follow from this:
- Every endpoint rejects requests without valid authentication, even if the “same user” just made an authenticated call.
- A request with an expired or revoked token fails, even mid-flow.
- Load balancers can route consecutive requests to different servers; behaviour should not change. (Flaky results that depend on which server you hit are a real class of bug.)
Collections: pagination, filtering and sorting
Collections can be large, so APIs paginate. You will meet three styles:
| Style | Example | Notes |
|---|---|---|
| Offset / limit | GET /orders?offset=40&limit=20 |
Simple; results can shift if data changes between pages |
| Page number | GET /orders?page=3&size=20 |
Same idea as offset, friendlier numbers |
| Cursor | GET /orders?limit=20&cursor=eyJpZCI6MTAyMH0 |
Stable under inserts; cursor is opaque |
A paginated response often looks like:
{
"data": [
{ "id": 1021, "status": "paid", "total": 49.9 },
{ "id": 1022, "status": "paid", "total": 12.5 }
],
"pagination": {
"limit": 2,
"nextCursor": "eyJpZCI6MTAyMn0",
"hasMore": true
}
}
Test cases for pagination, which are often under-tested:
- The first page, a middle page and the last page (where
hasMoreis false ornextCursoris null). limitat its boundaries:0,1, the documented maximum, and maximum + 1.- Invalid values: negative numbers, non-numeric strings, a tampered cursor.
- Walk all pages and check there are no duplicates and no missing items compared with the total.
- An empty collection returns
200with an empty array, not404.
Filtering and sorting deserve the same rigour: unknown filter fields, invalid sort directions, case sensitivity, and combinations of filters.
Versioning
APIs change. To avoid breaking existing clients, teams version them. Common approaches:
- URI versioning:
/v1/users,/v2/users. Most visible and most common. - Header versioning: a custom header such as
Api-Version: 2024-10-01, or a media type inAccept. - Query parameter:
/users?version=2.
For testers, versioning means regression testing old versions while new ones are developed. A change to v2 should not alter v1 responses. Contract tests and response schema checks are the efficient way to guard this.
Designing tests for a REST resource
Put it together. For any resource, a solid first pass covers:
- Happy path CRUD: create, read, update, delete, and verify each step with a follow-up GET.
- Validation: missing required fields, wrong types, boundary lengths, invalid formats.
- Not found: every id-based endpoint with a non-existent id.
- Auth: no token, invalid token, wrong role, accessing another user’s resource (an authorisation bug class called IDOR or BOLA).
- Idempotency: repeated PUT and DELETE.
- Collections: empty, single, paginated, filtered, sorted.
- Contract: response headers (
Content-Type,Location) and body schema.
Here is a compact example of the first two checks using Playwright’s API testing support, which you will use later in this path:
import { test, expect } from '@playwright/test';
test('create then read a user', async ({ request }) => {
const create = await request.post('/api/users', {
data: { name: 'Ana', email: `ana.${Date.now()}@example.com` },
});
expect(create.status()).toBe(201);
const created = await create.json();
const read = await request.get(`/api/users/${created.id}`);
expect(read.status()).toBe(200);
expect(await read.json()).toMatchObject({ id: created.id, name: 'Ana' });
});
test('rejects a user without an email', async ({ request }) => {
const res = await request.post('/api/users', { data: { name: 'Ana' } });
expect([400, 422]).toContain(res.status());
});
The request fixture uses baseURL from your Playwright config, so relative paths work. The API Test Case Templates tool can generate a fuller checklist.
Practice exercise
You are given an API for a product catalogue with these endpoints:
GET /products list (supports ?category=, ?limit=, ?cursor=)
GET /products/{id}
POST /products admin only
PATCH /products/{id} admin only
DELETE /products/{id} admin only
Write a test matrix with at least 12 test cases covering happy paths, validation, auth, not-found and pagination. For each, give the request and the expected status. Try a few against the API Playground if it offers a similar resource.
A good matrix includes both 401 and 403 cases, at least one pagination boundary, a PATCH that verifies untouched fields, and a DELETE repeated twice.
Show a model answer
| # | Request | Expected |
|---|---|---|
| 1 | GET /products |
200, array of products, Content-Type: application/json |
| 2 | GET /products?category=books |
200, every item has category equal to books |
| 3 | GET /products?category=does-not-exist |
200 with an empty array |
| 4 | GET /products?limit=1 |
200, exactly one item, a next cursor present |
| 5 | GET /products?limit=0 and ?limit=-1 |
400/422 per docs |
| 6 | Walk all pages via cursor | No duplicates; total equals the unpaginated count |
| 7 | GET /products/{existing id} |
200, matches schema |
| 8 | GET /products/{non-existent id} |
404 |
| 9 | POST /products as admin with valid body |
201, Location header, body echoes fields |
| 10 | POST /products with no token |
401 |
| 11 | POST /products as a normal customer |
403 |
| 12 | POST /products with negative price |
400/422 with a field-level error |
| 13 | PATCH /products/{id} with only price |
200; price changed, all other fields unchanged on re-read |
| 14 | DELETE /products/{id} then repeat |
First 204; second 404 (or 204 per docs); product no longer listed |
Key takeaways
- REST is an architectural style, not a standard; check the API against its own documentation and its own consistency.
- Learn the conventional CRUD to method to status code mapping so you can spot wrong responses instantly.
- PUT replaces, PATCH modifies. Always re-read the resource to confirm untouched fields stay untouched.
- Pagination, filtering and versioning are frequent sources of bugs; test boundaries, empty results and full traversals.
- A first-pass test plan for any resource covers CRUD, validation, not-found, auth, idempotency, collections and contract.
Key terms
Finished this lesson?
Progress is saved in this browser. No account needed.
Practise it
Related interview questions
- Which HTTP status codes matter in API testing? 401 vs 403, 400 vs 422, 200 vs 201 vs 204
- Explain REST Assured's given/when/then structure and write a basic test
- PUT vs PATCH (and POST), and what does idempotency mean?
- How do RequestSpecification and ResponseSpecification help you avoid duplication in REST Assured?