Whether you're a frontend developer integrating a backend, a QA engineer writing test suites, or a backend developer debugging your own endpoints, knowing how to test REST APIs is a fundamental skill. This guide takes you from zero to confidently testing any API.
We'll cover HTTP fundamentals, the most common testing patterns, authentication, error handling, and hands-on examples using curl, Postman, and browser-based tools. By the end, you'll be able to test any REST API you encounter.
What Is a REST API?
REST (Representational State Transfer) is an architectural style for building web APIs. A REST API exposes resources (data entities like users, orders, products) at URLs and uses standard HTTP methods to manipulate them.
The key principles:
- Stateless: Each request contains all information needed to process it. The server doesn't remember previous requests.
- Resource-based: Everything is a resource identified by a URL (
/api/users/123). - HTTP methods as verbs: GET reads, POST creates, PUT/PATCH updates, DELETE removes.
- Standard status codes: 200 OK, 201 Created, 404 Not Found, 500 Server Error.
HTTP Methods: The Big Five
Every REST API test starts with choosing the right HTTP method.
GET — Read Data
Retrieves a resource without modifying it. GET requests should be idempotent — calling them multiple times produces the same result.
GET /api/users → List all users
GET /api/users/123 → Get user with ID 123
GET /api/users?role=admin&limit=10 → Filtered list When testing GET requests, check:
- Response status is 200 OK
- Response body contains expected data structure
- Pagination headers or metadata are present for list endpoints
- Query parameters filter correctly
- Non-existent resource returns 404
POST — Create Data
Creates a new resource. The request body contains the data for the new resource.
POST /api/users
Content-Type: application/json
{
"name": "Alice",
"email": "[email protected]",
"role": "user"
} When testing POST requests, check:
- Response status is 201 Created (not 200)
- Response body contains the created resource with a generated ID
Locationheader points to the new resource URL- Duplicate creation is handled (409 Conflict or idempotency key)
- Missing required fields return 400 Bad Request with clear error messages
PUT — Replace Data
Replaces an entire resource. If the resource doesn't exist, some APIs create it (upsert behavior).
PUT /api/users/123
Content-Type: application/json
{
"name": "Alice Updated",
"email": "[email protected]",
"role": "admin"
} PUT should be idempotent — sending the same PUT request twice should have the same effect as sending it once.
PATCH — Partial Update
Updates specific fields without replacing the entire resource.
PATCH /api/users/123
Content-Type: application/json
{
"role": "admin"
}
Only the role field changes; name and email stay the same. PATCH is more bandwidth-efficient than PUT for small updates.
DELETE — Remove Data
DELETE /api/users/123 When testing DELETE, verify:
- Response is 204 No Content (or 200 with confirmation body)
- Subsequent GET returns 404
- Deleting a non-existent resource returns 404 (or 204 if idempotent)
- Cascade behavior: are related resources also deleted?
HTTP Status Codes You Must Know
| Code | Meaning | When You'll See It |
|---|---|---|
200 | OK | Successful GET, PUT, PATCH, DELETE |
201 | Created | Successful POST that creates a resource |
204 | No Content | Successful DELETE with no response body |
301/302 | Redirect | Resource moved; follow the Location header |
400 | Bad Request | Malformed JSON, missing required fields, invalid values |
401 | Unauthorized | Missing or invalid authentication |
403 | Forbidden | Authenticated but not authorized for this action |
404 | Not Found | Resource doesn't exist |
409 | Conflict | Duplicate creation, version conflict |
422 | Unprocessable Entity | Valid JSON but business rule violation |
429 | Too Many Requests | Rate limited; check Retry-After header |
500 | Internal Server Error | Server bug — not your fault (usually) |
502/503 | Bad Gateway / Unavailable | Server is down or overloaded |
Essential Headers
HTTP headers carry metadata about the request and response. Here are the ones you'll use in every API test:
Request Headers
Content-Type: application/json # "I'm sending JSON"
Accept: application/json # "I want JSON back"
Authorization: Bearer eyJhbGciOiJI... # Auth token
X-Request-ID: 550e8400-e29b-41d4 # Trace ID for debugging
If-None-Match: "etag-value" # Conditional GET (caching) Response Headers
Content-Type: application/json # Response format
Location: /api/users/456 # URL of created resource
X-RateLimit-Limit: 100 # Max requests per window
X-RateLimit-Remaining: 42 # Requests left
Retry-After: 30 # Seconds to wait (429)
ETag: "33a64df5" # Resource version for caching Testing with curl
curl is the universal API testing tool — available on every OS, scriptable, and precise. Here are the patterns you'll use daily.
GET Request
# Simple GET
curl https://api.example.com/users
# With headers and pretty-print
curl -s https://api.example.com/users \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" | jq . POST Request
# Create a user
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "[email protected]"}'
# From a file
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d @user.json PUT / PATCH / DELETE
# Full update
curl -X PUT https://api.example.com/users/123 \
-H "Content-Type: application/json" \
-d '{"name": "Alice Updated", "email": "[email protected]", "role": "admin"}'
# Partial update
curl -X PATCH https://api.example.com/users/123 \
-H "Content-Type: application/json" \
-d '{"role": "admin"}'
# Delete
curl -X DELETE https://api.example.com/users/123 \
-H "Authorization: Bearer YOUR_TOKEN" Useful curl Flags
-v # Verbose: show request/response headers
-s # Silent: no progress bar
-o /dev/null # Discard body (useful with -w)
-w "%{http_code}\n" # Print just the status code
-L # Follow redirects
-k # Skip TLS verification (dev only!)
--max-time 10 # Timeout after 10 seconds API Authentication Patterns
Most APIs require authentication. Here are the four most common patterns and how to test each.
1. API Key (Header or Query)
# In header (preferred)
curl -H "X-API-Key: your-api-key" https://api.example.com/data
# In query string (less secure)
curl "https://api.example.com/data?api_key=your-api-key" 2. Bearer Token (OAuth2 / JWT)
# Get token
curl -X POST https://auth.example.com/token \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_ID" \
-d "client_secret=YOUR_SECRET"
# Use token
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
https://api.example.com/protected 3. Basic Auth
# curl handles encoding automatically
curl -u "username:password" https://api.example.com/data
# Equivalent header
curl -H "Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=" \
https://api.example.com/data 4. Cookie-Based (Session)
# Login and save cookies
curl -c cookies.txt -X POST https://app.example.com/login \
-d '{"email": "[email protected]", "password": "secret"}'
# Use saved cookies
curl -b cookies.txt https://app.example.com/dashboard Testing Error Cases
Good API testing isn't just about the happy path. Here's a checklist of error cases every API should handle:
Validation Errors (400)
# Missing required field
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice"}'
# Expected: 400 with message about missing email
# Wrong data type
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "[email protected]", "age": "not-a-number"}'
# Expected: 400 with type validation error
# Empty body
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{}'
# Expected: 400 with list of missing fields Authentication Errors (401/403)
# No auth header
curl https://api.example.com/protected
# Expected: 401
# Expired token
curl -H "Authorization: Bearer expired-token" \
https://api.example.com/protected
# Expected: 401
# Valid token, wrong permissions
curl -H "Authorization: Bearer user-token" \
-X DELETE https://api.example.com/admin/users/123
# Expected: 403 Not Found (404)
# Non-existent resource
curl https://api.example.com/users/99999
# Expected: 404 with meaningful message
# Wrong URL
curl https://api.example.com/userz
# Expected: 404 Rate Limiting (429)
# Rapid-fire requests
for i in $(seq 1 100); do
curl -s -o /dev/null -w "%{http_code}\n" \
https://api.example.com/data
done
# Expected: 429 after exceeding limit, with Retry-After header Writing Automated API Tests
Once you're comfortable testing manually, automate your tests. Here's a simple example in JavaScript using fetch and a test runner:
// api.test.js (using Node.js built-in test runner)
import { describe, it } from 'node:test';
import assert from 'node:assert';
const BASE_URL = 'https://api.example.com';
describe('Users API', () => {
it('GET /users returns 200 and array', async () => {
const res = await fetch(`${BASE_URL}/users`);
assert.strictEqual(res.status, 200);
const data = await res.json();
assert.ok(Array.isArray(data));
assert.ok(data.length > 0);
});
it('POST /users creates a user', async () => {
const res = await fetch(`${BASE_URL}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Test User',
email: `test-${Date.now()}@example.com`
})
});
assert.strictEqual(res.status, 201);
const user = await res.json();
assert.ok(user.id);
assert.strictEqual(user.name, 'Test User');
});
it('GET /users/999999 returns 404', async () => {
const res = await fetch(`${BASE_URL}/users/999999`);
assert.strictEqual(res.status, 404);
});
it('POST /users without email returns 400', async () => {
const res = await fetch(`${BASE_URL}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'No Email' })
});
assert.strictEqual(res.status, 400);
});
}); Python with pytest
import requests
import pytest
BASE_URL = "https://api.example.com"
def test_get_users():
res = requests.get(f"{BASE_URL}/users")
assert res.status_code == 200
data = res.json()
assert isinstance(data, list)
assert len(data) > 0
def test_create_user():
payload = {"name": "Test", "email": f"test-{id(object())}@example.com"}
res = requests.post(f"{BASE_URL}/users", json=payload)
assert res.status_code == 201
assert "id" in res.json()
def test_not_found():
res = requests.get(f"{BASE_URL}/users/999999")
assert res.status_code == 404
def test_validation_error():
res = requests.post(f"{BASE_URL}/users", json={"name": "No Email"})
assert res.status_code == 400 API Testing Best Practices
- Test the contract, not the implementation. Your tests should verify the API's documented behavior — status codes, response shape, error messages. Don't test internal database state.
- Use unique test data. Generate unique emails, names, and IDs for each test run to avoid collisions. Timestamps or UUIDs work well.
- Clean up after yourself. If your test creates a user, delete it afterward. Use setup/teardown hooks or a dedicated test database.
- Test edge cases. Empty strings, very long strings, special characters, Unicode, null values, zero, negative numbers, boundary values (max int, empty arrays).
- Check response times. A test that passes in 30 seconds is a broken API. Set timeouts and assert on response time.
- Version your test collections. Keep API tests in version control alongside the code. They're documentation and regression safety nets.
- Test auth thoroughly. Missing tokens, expired tokens, wrong scopes, cross-tenant access — auth bugs are security bugs.
Test APIs Online — No Setup Required
Sometimes you need to quickly test an endpoint without setting up Postman or writing a script. DevToolkit's free API Tester lets you send any HTTP request right in your browser — GET, POST, PUT, PATCH, DELETE with custom headers, body, and authentication.
It shows the full response including status code, headers, timing, and a formatted JSON body. Perfect for quick debugging, exploring a new API, or sharing reproducible requests with your team. No signup, no install.
Common Mistakes When Testing APIs
- Forgetting Content-Type header. Many APIs return 400 or 415 if you send JSON without
Content-Type: application/json. - Testing only the happy path. If your tests only check successful responses, your app will break on the first 400 or 500.
- Hardcoding URLs and tokens. Use environment variables or config files. Test against staging, not production.
- Ignoring response headers. Rate limit headers, pagination links, cache headers — they're part of the API contract.
- Not checking response shape. A 200 with unexpected data is worse than a 500 — it fails silently downstream.
Conclusion
API testing is a core skill that pays dividends in every project. Start with manual testing using curl or a browser-based tool to understand the API, then automate your tests for regression safety. Always test both the happy path and error cases.
Ready to test an API right now? Open DevToolkit's API Tester — send requests, inspect responses, and debug endpoints in seconds. Free, in your browser, no setup needed.