REST API Testing and Debugging API
A REST API that is not tested is a promise waiting to be broken. Every endpoint, every parameter, and every error case needs verification before real users depend on it. This topic walks through how to test REST APIs thoroughly and fix problems when they appear.
Why API Testing Matters
Bugs in APIs are more damaging than bugs in a single app. One broken API can crash every mobile app, web app, and third-party integration that depends on it simultaneously. Testing catches those bugs before deployment.
Consider this scenario:
WITHOUT testing:
Developer changes response field name
"user_name" ──→ "username"
│
▼
Mobile app reads "user_name" ──→ gets null
Web app reads "user_name" ──→ gets null
Partner API reads "user_name"──→ gets null
│
▼
Three applications break in production
WITH testing:
Change is made ──→ automated test checks "user_name" exists
──→ test fails immediately ──→ developer fixes before merging
Types of API Tests
API testing is not one activity. It covers several layers, each catching a different class of problem.
┌─────────────────────────────────────────────────────────┐ │ API Testing Layers │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Contract Tests ← Does the response match docs?│ │ │ │ ┌───────────────────────────────────────────┐ │ │ │ │ │ Integration Tests ← Do services talk ok? │ │ │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │ │ │ │ Unit Tests ← Does each function │ │ │ │ │ │ │ │ return correct output? │ │ │ │ │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ └───────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘
Unit Tests
Unit tests check individual functions or methods inside your API code — no network, no database. They run in milliseconds and pinpoint exactly which function broke.
Integration Tests
Integration tests check that your API, database, and any external services work correctly together. They use a real (or test) database and send real HTTP requests to your running API server.
Contract Tests
Contract tests verify that your API's actual responses match the promises made in your OpenAPI documentation. If the docs say a field is always present, the contract test confirms it is never missing.
End-to-End Tests
End-to-end tests run a full user workflow across multiple endpoints — register, login, create a resource, retrieve it, delete it — and verify every step succeeds in sequence.
What to Test on Every Endpoint
For each endpoint in your API, test these scenarios without exception:
Endpoint: POST /orders Test Checklist: ┌─────────────────────────────────────────────────────┐ │ ✓ Happy path — valid input → 201 Created │ │ ✓ Missing required field → 422 Unprocessable │ │ ✓ Wrong data type → 400 Bad Request │ │ ✓ No auth token → 401 Unauthorized │ │ ✓ Valid token, no permission → 403 Forbidden │ │ ✓ Resource already exists → 409 Conflict │ │ ✓ Duplicate request (idempotency) → same result │ │ ✓ Extremely large payload → handled gracefully │ │ ✓ Response has correct Content-Type header │ │ ✓ Response body matches documented schema │ └─────────────────────────────────────────────────────┘
Testing with Postman
Postman is the most widely used GUI tool for API testing. It lets you send HTTP requests, inspect responses, write automated tests, and organize everything into collections.
Setting Up a Postman Request
┌──────────────────────────────────────────────────────┐
│ Postman Request Builder │
│ │
│ Method: [POST ▼] │
│ URL: https://api.store.com/v1/products │
│ │
│ Tabs: [Headers] [Body] [Auth] [Tests] │
│ │
│ Body (raw → JSON): │
│ { │
│ "name": "Blue Pen", │
│ "price": 25, │
│ "stock": 100 │
│ } │
│ │
│ Auth: Bearer Token → {{token}} ← variable │
└──────────────────────────────────────────────────────┘
Using {{token}} as a variable means you set the real token once in your environment, and all requests use it automatically.
Writing Tests in Postman
Postman's Test tab runs JavaScript after each request. Write assertions there to check the response automatically.
// Test 1: Status code is 201
pm.test("Status is 201 Created", function () {
pm.response.to.have.status(201);
});
// Test 2: Response has an id field
pm.test("Response contains id", function () {
const body = pm.response.json();
pm.expect(body).to.have.property("id");
});
// Test 3: Response time is acceptable
pm.test("Response under 500ms", function () {
pm.expect(pm.response.responseTime).to.be.below(500);
});
// Test 4: Save the new product id for next request
const body = pm.response.json();
pm.environment.set("product_id", body.id);
That last line stores the new product's ID so the next request in your collection can use it — this is how you chain requests to test a full workflow.
Running a Postman Collection
Collection: "Order Workflow"
│
├── Step 1: POST /auth/login → saves token to env variable
├── Step 2: POST /products → creates product, saves id
├── Step 3: GET /products/{{id}} → verifies product exists
├── Step 4: POST /orders → creates order with product id
├── Step 5: GET /orders/{{order_id}} → confirms order details
└── Step 6: DELETE /orders/{{id}} → cleans up test data
Click "Run Collection" in Postman's Collection Runner and all six steps execute in order, with test results reported for each one.
Testing with curl
curl is a command-line tool available on every operating system. Developers use it to send quick HTTP requests without a GUI.
GET Request
curl https://api.example.com/v1/users
POST Request with JSON Body
curl -X POST https://api.example.com/v1/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGc..." \
-d '{"name": "Alice", "email": "alice@example.com"}'
Useful curl Flags
Flag What It Does
────────────────────────────────────────────────
-v Verbose — shows request headers sent
and response headers received
-i Include response headers in output
-o file.json Save response body to a file
-s Silent mode — no progress bar
--max-time 5 Fail if request takes longer than 5s
Automated API Testing with Code
Postman and curl are great for manual testing. For automated pipelines, write tests in code. Here is what a simple integration test looks like in Python using pytest and the requests library:
import requests
BASE = "https://api.example.com/v1"
def test_create_user():
payload = {"name": "Bob", "email": "bob@test.com"}
response = requests.post(f"{BASE}/users", json=payload)
# Check status code
assert response.status_code == 201
data = response.json()
# Check required fields exist
assert "id" in data
assert data["name"] == "Bob"
assert data["email"] == "bob@test.com"
def test_get_missing_user():
response = requests.get(f"{BASE}/users/99999")
# Should be 404, not 500
assert response.status_code == 404
error = response.json()
assert "message" in error
Run these tests with pytest and they pass or fail automatically. Add them to your CI/CD pipeline so they run on every code push.
Debugging API Problems
When an API call fails, follow a systematic process to find the cause. Random guessing wastes time.
Step 1 — Read the Status Code
Status Code First Question to Ask ────────────────────────────────────────────────────── 400 Did the client send bad data? 401 Is the auth token present and valid? 403 Does this user have permission? 404 Does the resource exist at this URL? 405 Did I use the right HTTP method? 415 Did I send the right Content-Type? 422 Does the body match the required schema? 429 Am I sending too many requests? 500 Is there a bug or exception on the server? 503 Is the server or database down?
Step 2 — Read the Response Body
Most well-designed APIs send an error message in the response body. Read it before doing anything else.
HTTP/1.1 422 Unprocessable Entity
{
"error": "validation_failed",
"message": "Field 'email' is required",
"field": "email"
}
This response tells you exactly what is wrong — no guessing needed.
Step 3 — Check Request Headers
Common missing or wrong headers: Header Expected Value ───────────────────────────────────────────────────── Content-Type application/json Authorization Bearer← check expiry Accept application/json X-API-Version 2 ← if required
Step 4 — Inspect with Browser Dev Tools
Open your browser's Developer Tools, go to the Network tab, and click the failing API request. You see:
┌─────────────────────────────────────────────────┐
│ Network Tab → Request Details │
│ │
│ Request URL: /api/v1/orders/55 │
│ Request Method: DELETE │
│ Status Code: 403 Forbidden │
│ │
│ Request Headers: │
│ Authorization: Bearer eyJ... ← inspect this │
│ │
│ Response Body: │
│ {"error": "not_owner", │
│ "message": "You don't own this order"} │
└─────────────────────────────────────────────────┘
Step 5 — Check Server Logs
If the response body does not explain the error, check your server logs. A 500 error almost always has a stack trace in the logs that points to the exact line of broken code.
Log entry for a 500 error:
──────────────────────────────────────────────────
ERROR 2024-06-01 14:32:01 orders.service.js:88
TypeError: Cannot read property 'price' of undefined
at calculateTotal (orders.service.js:88:20)
at POST /orders (orders.controller.js:34:5)
──────────────────────────────────────────────────
Line 88 of orders.service.js tries to read .price
from an object that is undefined — a null check is missing.
Load Testing Your API
Load testing checks how your API behaves under heavy traffic — not just whether it works for one user, but whether it holds up for thousands.
Key Metrics to Measure
┌───────────────────────────────────────────────────────┐ │ Load Test Dashboard │ │ │ │ Total Requests: 50,000 │ │ Duration: 5 minutes │ │ Concurrent Users: 200 │ │ │ │ Avg Response Time: 145ms ← target: under 300ms │ │ 95th Percentile: 312ms ← 5% of requests are slow │ │ 99th Percentile: 890ms ← 1% are very slow │ │ │ │ Error Rate: 0.3% ← target: under 1% │ │ Requests/sec: 167 │ └───────────────────────────────────────────────────────┘
Simple Load Test with k6
k6 is an open-source load testing tool. Write test scripts in JavaScript.
import http from "k6/http";
import { sleep, check } from "k6";
export let options = {
vus: 50, // 50 virtual users
duration: "30s", // run for 30 seconds
};
export default function () {
const res = http.get("https://api.example.com/v1/products");
check(res, {
"status is 200": (r) => r.status === 200,
"response under 400ms": (r) => r.timings.duration < 400,
});
sleep(1); // each user waits 1 second between requests
}
Testing Error Handling
Many developers only test the happy path — what happens when everything works. Always test what happens when things go wrong.
Error Scenario Testing Matrix
Scenario How to Test It
──────────────────────────────────────────────────────────
Database is down Stop the DB, call the API
Expect: 503, not 500
Invalid JSON in body Send malformed JSON string
Expect: 400, helpful message
Token expired Use a token from yesterday
Expect: 401, not 403
Giant payload Send 50MB body
Expect: 413 or graceful reject
SQL injection attempt Send "'; DROP TABLE users;--"
Expect: 400 or 422, no crash
Concurrent same request Send same POST twice at once
Expect: one 201, one 409
API Mocking for Testing Dependencies
When your API calls an external service (payment gateway, SMS provider), you cannot send real money or messages during tests. Use mocking.
Real Flow (production):
Your API ──→ Stripe API ──→ charges card ──→ returns payment result
Test Flow (with mock):
Your API ──→ Mock Stripe ──→ returns fake result ──→ no real charge
│
└── You control what the mock returns:
success, card declined, timeout, server error
Tools like WireMock, Mockoon, and Nock let you create mock servers that return predefined responses for specific requests.
Setting Up API Tests in CI/CD
Developer pushes code to GitHub
│
▼
CI pipeline starts automatically
│
▼
Step 1: Build the application
│
▼
Step 2: Start a test database
│
▼
Step 3: Run unit tests ← fast, must pass
│
▼
Step 4: Start the API server
│
▼
Step 5: Run integration tests ← slower, must pass
│
▼
Step 6: Run contract tests ← verify docs match
│
▼
All green? → Allow merge to main branch
Any red? → Block merge, notify developer
Common API Bugs and Their Causes
Bug Observed Likely Cause ───────────────────────────────────────────────────────── 200 but empty body Missing return statement 404 on valid resource Wrong URL path variable 401 even with valid token Token parsing strips "Bearer " 500 on some inputs only Missing null/undefined check Correct data, wrong order No ORDER BY in database query Data disappears between Using in-memory storage requests instead of a database CORS error in browser Missing CORS headers on server Very slow response N+1 database query problem
Key Points
- Always test happy paths, error paths, missing fields, wrong types, and edge cases for every endpoint.
- Postman is the fastest tool for manual API testing and chaining multi-step workflows.
- Read the status code first, then the response body — most well-designed APIs tell you exactly what went wrong.
- Write automated tests in code and run them in CI/CD so every code change is verified automatically.
- Load test your API before launch to find performance limits before real users hit them.
- Mock external services in tests so you never send real transactions or messages during test runs.
- Server logs reveal the true cause of 500 errors — always check them when the response body is not helpful.
