DEV Community

Cover image for REST API Testing: What Every QA Engineer Must Know
idavidov13
idavidov13

Posted on • Originally published at idavidov.eu

REST API Testing: What Every QA Engineer Must Know

Imagine this: you test a POST endpoint that creates a new user. It returns 201 Created. You mark the test as passed and move on. Two weeks later, production breaks - the response was missing a required field, a price came back as a string instead of a number, and nobody validated the response schema. Sound familiar?

REST API testing goes far beyond "send a request, check the status code". A solid API test verifies the method, the parameters, the payload, the status code, the response body, and the response structure. Miss any one of those, and bugs slip through.

This guide covers everything you need to test REST APIs thoroughly - from HTTP methods and payloads to status codes and schema validation. No tool-specific code, no fluff. Just the engineering fundamentals that apply everywhere.


🧭 What Is a REST API

Think of a REST API as a restaurant's ordering system. You (the client) send an order (a request) to the kitchen (the server). The kitchen processes it and sends back your food (the response). The menu defines what you can order (the endpoints), and the waiter gives you feedback on how it went (the status code).

REST (Representational State Transfer) is an architectural style where clients communicate with servers over HTTP. Every "thing" in the system - a user, an order, a product - is a resource, identified by a URL. You interact with resources using standard HTTP methods, and data travels back and forth as JSON.

That's it. Client sends a request, server sends a response. Everything we test revolves around this exchange.


πŸ“¬ HTTP Methods - The Five Actions

Every API request starts with a method. The method tells the server what you want to do with a resource. There are five you'll use constantly.

Method Purpose Idempotent Has Request Body
GET Retrieve data Yes No
POST Create a new resource No Yes
PUT Replace an entire resource Yes Yes
PATCH Partially update a resource Usually* Yes
DELETE Remove a resource Yes Rarely

Idempotent means calling the same request multiple times produces the same result. A GET request for user #42 always returns user #42. A POST that creates an order will create a new one every time - that's why it's not idempotent.

*PATCH is typically idempotent, but not guaranteed. An operation like {"increment": 1} would add 1 every time you call it - that's non-idempotent. Always verify the specific API's behavior.

The five HTTP methods as distinct actions

GET - Retrieves a resource without changing anything on the server. Think of it as reading a menu without placing an order.

Request:

GET /api/users/42
Enter fullscreen mode Exit fullscreen mode

Response body (200 OK):

{
  "id": 42,
  "name": "Jane Smith",
  "email": "jane.smith@example.com",
  "role": "developer"
}
Enter fullscreen mode Exit fullscreen mode

POST - Creates a new resource. You send the data, and the server assigns an ID and stores it.

Request:

POST /api/users
Enter fullscreen mode Exit fullscreen mode

Request payload:

{
  "name": "John Doe",
  "email": "john.doe@example.com",
  "role": "tester"
}
Enter fullscreen mode Exit fullscreen mode

Response body (201 Created):

{
  "id": 87,
  "name": "John Doe",
  "email": "john.doe@example.com",
  "role": "tester",
  "createdAt": "2026-04-16T10:30:00Z"
}
Enter fullscreen mode Exit fullscreen mode

PUT vs PATCH - This is where most confusion happens. PUT replaces the entire resource. If you send a PUT without a required field, the API will most likely reject it with a 400 Bad Request. PATCH updates only the fields you include.

Request:

PUT /api/users/42
Enter fullscreen mode Exit fullscreen mode

Request payload (full resource replacement):

{
  "name": "Jane Smith",
  "email": "jane.updated@example.com",
  "role": "developer"
}
Enter fullscreen mode Exit fullscreen mode

Every field must be present. Miss a required field like role, and the API will either reject the request with 400 Bad Request or set the value to null - depending on the implementation. Both behaviors are worth testing.

Request:

PATCH /api/users/42
Enter fullscreen mode Exit fullscreen mode

Request payload (partial update):

{
  "email": "jane.updated@example.com"
}
Enter fullscreen mode Exit fullscreen mode

Only the email changes. Everything else stays untouched.

DELETE - Removes a resource. Usually returns 204 No Content with an empty body.

Request:

DELETE /api/users/42
Enter fullscreen mode Exit fullscreen mode

Response: 204 No Content (empty body).

What to test for each method:

  • Happy path - Does the server return the correct success status code? (200 for GET, PUT, and PATCH, 201 for POST, 204 for DELETE)
  • Side effects - Did the operation actually happen? After a POST, can you GET the new resource? After a DELETE, does a GET return 404?
  • Response body - Does it contain all expected fields with correct values and types?
  • Missing or invalid data - Does a POST with a missing required field return 400? Does a PUT with the wrong data type return 400?
  • Non-existent resources - Does a GET, PUT, PATCH, or DELETE for an ID that doesn't exist return 404?
  • Authentication and authorization - Does the endpoint return 401 without a token and 403 with insufficient permissions?
  • Duplicate/conflict - Does a POST with data that already exists (e.g., a duplicate email) return 409 Conflict?
  • Method not supported - Does sending a DELETE to a read-only endpoint return 405?
  • Response headers - Does every response return Content-Type: application/json?

πŸ“¨ Request Headers - The Metadata Layer

Headers are the metadata of your request. They travel alongside the URL and body, telling the server how to interpret the request, who you are, and what format you expect back.

Think of headers like the shipping label on a package. The package contents are your payload, but the label tells the courier where it's going, how fragile it is, and who sent it. Without the right label, even a perfect package won't reach its destination.

Headers every API tester should know:

Header Purpose Example Value
Authorization Identifies who you are Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type Tells the server what format you're sending application/json
Accept Tells the server what format you want back application/json
X-Request-ID Traces a request through distributed systems req-abc-123-def-456
Cache-Control Controls caching behavior no-cache

Authorization patterns:

Most APIs use one of these authentication methods:

Bearer tokens - The most common pattern. You send a token in the Authorization header.

GET /api/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Enter fullscreen mode Exit fullscreen mode

API keys - Simpler but less secure. Often sent as a header or query parameter.

GET /api/products
X-API-Key: sk_live_abc123def456
Enter fullscreen mode Exit fullscreen mode

What to test for headers:

  • Missing Authorization - Does the API return 401 Unauthorized when no token is provided?
  • Invalid token - Does an expired, malformed, or revoked token return 401?
  • Insufficient permissions - Does a valid token with wrong role return 403 Forbidden?
  • Wrong Content-Type - What happens when you send JSON but declare Content-Type: text/plain?
  • Missing Content-Type - Does the API assume JSON, reject the request, or crash?
  • Accept header mismatch - If you request Accept: application/xml but the API only supports JSON, what happens?

πŸ” Query Parameters - Filtering, Sorting, Paging

Query parameters live in the URL after a ? sign. They don't change the resource - they change how you retrieve it. Think of them as filters in an online store: "Show me electronics, sorted by price, cheapest first, second page, 20 items per page."

GET /api/products?category=electronics&sort=price&order=asc&page=2&limit=20
Enter fullscreen mode Exit fullscreen mode

This request says: "Give me electronics, sorted by price ascending, second page, 20 items per page."

A typical paginated response looks like this:

{
  "data": [
    { "id": 21, "name": "Wireless Mouse", "price": 29.99 },
    { "id": 22, "name": "USB-C Hub", "price": 34.99 }
  ],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 47,
    "totalPages": 3,
    "hasNext": true,
    "hasPrevious": true
  }
}
Enter fullscreen mode Exit fullscreen mode

When testing pagination, verify all these metadata fields - not just the items in data.

Common query parameter patterns:

Pattern Example Purpose
Filtering ?status=active Return only matching items
Sorting ?sort=createdAt&order=desc Control result order
Pagination ?page=1&limit=25 Return given page and Limit results per it
Searching ?q=wireless+headphones Full-text search
Field selection ?fields=id,name,price Return only specific fields

What to test:

  • Missing parameters - What happens when you omit page or limit? The API should use sensible defaults, not crash.
  • Invalid values - ?page=-1, ?limit=abc, ?sort=nonExistentField. Expect a 400 Bad Request, not a 500.
  • Boundary values - ?limit=0, ?limit=10000, ?page=999999. Does the API handle extremes gracefully?
  • Empty results - A valid filter that matches nothing should return an empty array with 200, not a 404.
  • Combinations - Test filters together. Does ?category=electronics&status=active return items that match both conditions?

πŸ“¦ Request Payloads - What You Send Matters

The request payload (or body) is the data you send with POST, PUT, and PATCH requests. It's typically JSON, and it needs a Content-Type: application/json header so the server knows how to parse it.

A payload has a defined structure. Each field has a name, a data type, and constraints (required or optional, minimum/maximum length, allowed values). Here's a typical one:

{
  "name": "Wireless Headphones",
  "description": "Noise-cancelling over-ear headphones",
  "price": 149.99,
  "currency": "USD",
  "category": "electronics",
  "inStock": true,
  "tags": ["wireless", "noise-cancelling", "bluetooth"],
  "specifications": {
    "weight": "250g",
    "batteryLife": "30 hours",
    "connectivity": "Bluetooth 5.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice the variety: strings, numbers, booleans, arrays, and objects. Each type needs different testing strategies. And that's exactly what the next section is about.


πŸ§ͺ Testing Payload Parameters Like a QA Engineer

Sending a valid payload is the easy part. The real value of API testing comes from answering: what happens when the payload is wrong?

A systematic approach tests each field across multiple dimensions. Here's the framework.

Required vs Optional Fields

For every required field, test what happens when it's missing from the payload entirely. The API should return 400 Bad Request with a clear error message - not silently accept incomplete data.

{
  "description": "Missing the required 'name' field",
  "price": 29.99,
  "category": "electronics"
}
Enter fullscreen mode Exit fullscreen mode

Expected: 400 Bad Request with a message like "name is required".

Data Type Violations

Send the wrong type for each field. A price should be a number - what happens when you send a string?

Field Expected Type Test With Expected Result
price number "free" 400 Bad Request
inStock boolean "yes" 400 Bad Request
tags array "single-tag" 400 Bad Request
name string 12345 400 Bad Request

Boundary Values

Every field with constraints has edges worth testing.

Test Case Input Expected Result
Empty string "name": "" 400 (if name is required)
Minimum length "name": "A" Depends on min constraint
Maximum length "name": "A" Γ— 256 chars 400 if exceeds max
Negative number "price": -10 400 (prices can't be negative)
Zero "price": 0 Depends on business rules
Very large number "price": 999999999.99 Depends on max constraint

Null, Empty, and Missing

These three are different, and many bugs hide in the distinction.

{ "name": null }
{ "name": "" }
{ }
Enter fullscreen mode Exit fullscreen mode

A field set to null, a field set to an empty string, and a field that's absent entirely may all be handled differently by the API. Test all three.

Special Characters and Injection

What happens when you send <script>alert('xss')</script> as a name? The API should sanitize or reject these inputs, never execute them.

A secure API handles this in one of two ways.

Request payload:

{
  "name": "<script>alert('xss')</script>",
  "email": "test@example.com"
}
Enter fullscreen mode Exit fullscreen mode

Response (rejection approach) - 400 Bad Request:

{
  "error": {
    "code": "INVALID_INPUT",
    "message": "Name contains invalid characters"
  }
}
Enter fullscreen mode Exit fullscreen mode

Response (sanitization approach) - 201 Created:

{
  "id": 88,
  "name": "&lt;script&gt;alert('xss')&lt;/script&gt;",
  "email": "test@example.com"
}
Enter fullscreen mode Exit fullscreen mode

Either response is acceptable. What's never acceptable is storing and returning the raw malicious input unchanged.

Field Interaction

Some fields depend on each other. If currency is required when price is present, test sending price without currency. If endDate must be after startDate, test sending them in reverse order.

The goal isn't to break the API for fun. It's to verify that the API protects itself - and its data - from every kind of invalid input.


🚦 Status Codes - The API's Way of Talking Back

If the request is what you say to the API, the status code is its body language. It tells you immediately whether things went well, whether you messed up, or whether the server had a problem.

Status codes are grouped into three families that matter for testing.

2xx - "Everything went well"

Code Name When It's Returned What to Verify
200 OK Successful GET, PUT, PATCH Response body contains the expected data
201 Created Successful POST Response body contains the new resource with a server-assigned id
202 Accepted Async operations (background jobs, batch processing) Response confirms the request was accepted. May include a job ID or status URL for polling
204 No Content Successful DELETE, or updates that return no body Response body is empty. Don't try to parse it

The difference between 200 and 201 matters. A POST that returns 200 instead of 201 is technically working, but it's not following REST conventions - and that's a valid bug to report.

204 is the quiet confirmation. The API did what you asked but has nothing to say about it. After a DELETE, this is exactly what you want.

4xx - "You made a mistake"

These are client errors. The request was wrong in some way, and the server is telling you what happened.

Code Name Common Cause Example Scenario
400 Bad Request Malformed JSON, wrong data types, missing required fields Sending "price": "abc" instead of a number
401 Unauthorized Missing or invalid authentication token Calling an endpoint without an Authorization header
403 Forbidden Valid auth, but insufficient permissions A regular user trying to access an admin-only endpoint
404 Not Found Resource doesn't exist, or wrong URL GET /api/users/99999 when that user doesn't exist
405 Method Not Allowed Using the wrong HTTP method Sending a DELETE to an endpoint that only supports GET
409 Conflict Resource state conflict Creating a user with an email that already exists
429 Too Many Requests Rate limit exceeded Sending 100 requests per second to an endpoint limited to 10

The distinction between 401 and 403 is critical and often confused. 401 means "I don't know who you are" - provide credentials. 403 means "I know who you are, but you're not allowed to do this" - different credentials won't help unless you have a higher permission level.

What a good error response looks like:

A well-designed API returns structured error responses that help you understand exactly what went wrong.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" },
      { "field": "price", "message": "Must be a positive number" }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

When testing errors, verify the response includes all three levels: a machine-readable code, a human-readable message, and field-level details when applicable.

What to test for 4xx codes:

  • Verify the response includes a meaningful error message, not just the status code
  • Check that error messages don't leak sensitive information (stack traces, database details, internal paths)
  • Confirm the server didn't partially process the request. A 400 on a creation endpoint should mean nothing was created
  • Verify field-level errors point to the correct field name

5xx - "The server broke"

These are server-side errors. The client did nothing wrong, but the server couldn't fulfill the request.

Code Name What It Means Testing Angle
500 Internal Server Error Unhandled exception on the server This should never happen with valid input. If it does, that's a bug - report it with the exact request that triggered it
502 Bad Gateway The server received an invalid response from an upstream service Common in microservice architectures. Test when a dependency is down
503 Service Unavailable Server is overloaded or under maintenance May include a Retry-After header. Verify the API returns this during deployments or heavy load
504 Gateway Timeout An upstream service took too long to respond Similar to 502 but specifically about timing. Test with slow-responding dependencies

The golden rule for 5xx codes: a properly built API should never return 500 in response to any client input, no matter how bizarre. If you can trigger a 500 by sending unexpected data, that's a bug in the API's error handling. The server should catch internal errors and return an appropriate 4xx with a helpful message.

Here's a quick mental model for the full picture:

Family Who's at fault? Your reaction as a tester
2xx Nobody - it worked Verify the response body and schema
4xx The client Verify error messages are clear and no data was modified
5xx The server Report it - this is always a bug

Status code families as traffic signals


πŸ›‘οΈ Schema Validation - The Safety Net You're Probably Missing

You've checked the status code. You've checked a few fields in the response. But have you checked the entire structure of the response?

Schema validation is like checking your order at a restaurant. The waiter brings your plate and smiles - that's your 200 OK. But do you just trust the smile?

You check that every dish you ordered is actually there, that the steak is steak and not chicken, that the side is fries and not salad, and that nothing extra ended up on the plate. Schema validation does exactly this for API responses.

Why status code alone isn't enough:

A 200 OK response that looks like this is a problem:

{
  "id": 42,
  "name": "Jane Smith",
  "email": null,
  "role": "developer",
  "salary": "85000"
}
Enter fullscreen mode Exit fullscreen mode

The status code says "success," but email is null when it shouldn't be, and salary is a string instead of a number. Without schema validation, your test passes. With it, you catch the issue immediately.

What to validate in a response schema:

Check What It Catches
Field presence Missing fields that should always be there
Data types A number returned as a string, a boolean as "true"
Required vs optional A required field came back as null or was absent
Array structure An array that should contain objects contains strings instead
Nested objects A nested address object is missing the zipCode field
Enum values A status field returns "actve" (typo) instead of "active"
Format An email field contains "not-an-email", a date field contains "yesterday"

JSON Schema in practice:

JSON Schema is a standard that lets you define exactly what a valid response looks like. Here's a schema for a user response:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["id", "name", "email", "role"],
  "properties": {
    "id": {
      "type": "integer",
      "minimum": 1
    },
    "name": {
      "type": "string",
      "minLength": 1
    },
    "email": {
      "type": "string",
      "format": "email"
    },
    "role": {
      "type": "string",
      "enum": ["developer", "tester", "manager", "admin"]
    },
    "createdAt": {
      "type": "string",
      "format": "date-time"
    }
  },
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

This schema enforces that id is a positive integer, name is a non-empty string, email follows an email format, role is one of four allowed values, and no unexpected fields are present. If the API returns anything that doesn't match, the validation fails - and your test catches it.

When schema validation saves you:

  • A developer adds a new field to the response but forgets to update the documentation. additionalProperties: false catches it.
  • A database migration changes a column type from integer to string. Your schema says "type": "integer" - caught.
  • A null value sneaks in because of a missing database join. The required array catches it.
  • An enum value is misspelled. The enum constraint catches it.

Schema validation as the safety net

Schema validation doesn't replace field-level assertions. It complements them. Check the schema for structure, then assert specific values for business logic.


πŸ—ΊοΈ Putting It All Together - The API Test Checklist

Here's the practical checklist you can use for every endpoint. Think of it as the minimum bar for thorough API test coverage.

For every endpoint, verify:

1. Method behavior

  • Does the endpoint respond to the correct HTTP method?
  • Does it return 405 Method Not Allowed for unsupported methods?
  • Is the operation idempotent where it should be?

2. Query parameters (GET endpoints)

  • Do filters return only matching results?
  • Do defaults apply when parameters are omitted?
  • Are invalid parameter values rejected with 400?
  • Does pagination work correctly at boundaries (first page, last page, beyond last page)?

3. Request payload (POST/PUT/PATCH)

  • Are required fields enforced?
  • Are data types validated?
  • Are boundary values handled correctly?
  • Is null/empty/missing treated appropriately for each field?
  • Are field dependencies enforced?

4. Status codes

  • Does the happy path return the correct 2xx code (not just "any 200")?
  • Does invalid input return the correct 4xx code with a clear error message?
  • Can you trigger a 5xx with any input? (If yes, report it - that's a bug)

5. Response body

  • Does the response contain all expected fields?
  • Are field values correct (not just present)?
  • Are related resources updated? (After POST, does GET return the new item?)

6. Schema validation

  • Does the response match the defined JSON Schema?
  • Are data types correct for every field?
  • Are required fields always present and non-null?
  • Are enum values within the allowed set?

The testing checklist as layers of verification

This checklist applies regardless of your tool - whether you're using Postman, curl, Playwright, REST Assured, or any other framework. The principles are universal.

Ready to put this into practice? Implementing API Tests shows you how to build these concepts into a real Playwright test suite with fixtures and schema validation.

For a structured path through more QA engineering fundamentals, the Complete Roadmap to QA Automation & Engineering organizes everything by series and learning order.


πŸ™πŸ» Thank you for reading!

Next time you write an API test, I hope this guide saves you from the "it returned 200, ship it" trap. Test the structure, test the edge cases, and validate that schema.

Top comments (0)