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.
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
Response body (200 OK):
{
"id": 42,
"name": "Jane Smith",
"email": "jane.smith@example.com",
"role": "developer"
}
POST - Creates a new resource. You send the data, and the server assigns an ID and stores it.
Request:
POST /api/users
Request payload:
{
"name": "John Doe",
"email": "john.doe@example.com",
"role": "tester"
}
Response body (201 Created):
{
"id": 87,
"name": "John Doe",
"email": "john.doe@example.com",
"role": "tester",
"createdAt": "2026-04-16T10:30:00Z"
}
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
Request payload (full resource replacement):
{
"name": "Jane Smith",
"email": "jane.updated@example.com",
"role": "developer"
}
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
Request payload (partial update):
{
"email": "jane.updated@example.com"
}
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
Response: 204 No Content (empty body).
What to test for each method:
-
Happy path - Does the server return the correct success status code? (
200for GET, PUT, and PATCH,201for POST,204for 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 return400? -
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
401without a token and403with 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...
API keys - Simpler but less secure. Often sent as a header or query parameter.
GET /api/products
X-API-Key: sk_live_abc123def456
What to test for headers:
-
Missing Authorization - Does the API return
401 Unauthorizedwhen 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/xmlbut 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
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
}
}
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
pageorlimit? The API should use sensible defaults, not crash. -
Invalid values -
?page=-1,?limit=abc,?sort=nonExistentField. Expect a400 Bad Request, not a500. -
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 a404. -
Combinations - Test filters together. Does
?category=electronics&status=activereturn 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"
}
}
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"
}
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": "" }
{ }
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"
}
Response (rejection approach) - 400 Bad Request:
{
"error": {
"code": "INVALID_INPUT",
"message": "Name contains invalid characters"
}
}
Response (sanitization approach) - 201 Created:
{
"id": 88,
"name": "<script>alert('xss')</script>",
"email": "test@example.com"
}
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" }
]
}
}
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
400on 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 |
π‘οΈ 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"
}
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
}
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: falsecatches 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
requiredarray catches it. - An enum value is misspelled. The
enumconstraint catches it.
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 Allowedfor 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?
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)