Skip to content
SDETRole
SDETRole

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/42 rather 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 hasMore is false or nextCursor is null).
  • limit at 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 200 with an empty array, not 404.

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 in Accept.
  • 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:

  1. Happy path CRUD: create, read, update, delete, and verify each step with a follow-up GET.
  2. Validation: missing required fields, wrong types, boundary lengths, invalid formats.
  3. Not found: every id-based endpoint with a non-existent id.
  4. Auth: no token, invalid token, wrong role, accessing another user’s resource (an authorisation bug class called IDOR or BOLA).
  5. Idempotency: repeated PUT and DELETE.
  6. Collections: empty, single, paginated, filtered, sorted.
  7. 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.

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →