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.

Leave a Comment