apitestingrest
REST API Testing Techniques
· Cosyslabs
REST API testing verifies that HTTP endpoints behave correctly across all conditions — success paths, error cases, authentication, rate limiting, and edge inputs. Unlike UI testing, API tests run fast, are easy to automate, and are not affected by frontend changes. Every API should have a comprehensive test suite before shipping to production.
What to Test in a REST API
A complete API test suite covers:
- Status codes: 200, 201, 400, 401, 403, 404, 422, 429, 500
- Response shape: required fields present, correct types, no unexpected fields
- Headers: Content-Type, CORS, Cache-Control, rate limit headers
- Authentication: valid tokens work, expired tokens return 401, missing tokens return 401
- Validation: invalid inputs return 400/422 with descriptive error messages
- Business logic: correct data is returned, side effects occur as expected
- Idempotency: PUT/DELETE produce the same result when called multiple times
Manual Testing with curl
curl is the fastest way to test an API from the command line:
# GET request with auth header
curl -s -X GET "https://api.example.com/users/123" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9..." \
-H "Accept: application/json" | jq .
# POST with JSON body
curl -s -X POST "https://api.example.com/users" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"name": "Alice", "email": "alice@example.com"}' \
| jq .
# Include response headers (-i) and time the request (-w)
curl -i -w "\nTime: %{time_total}s\n" \
-X GET "https://api.example.com/health"
# Test with an invalid token (expect 401)
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer invalid-token" \
"https://api.example.com/protected"
# → 401
Automated Testing with Jest + Supertest (Node.js)
Supertest lets you test Express/Fastify/Koa apps directly without starting an HTTP server:
import request from "supertest";
import { app } from "../src/app";
import { db } from "../src/db";
describe("GET /users/:id", () => {
beforeAll(async () => {
await db.connect();
await db.user.create({ data: { id: "usr_123", name: "Alice", email: "alice@example.com" } });
});
afterAll(async () => {
await db.user.deleteMany();
await db.disconnect();
});
it("returns the user when authenticated", async () => {
const token = generateTestToken("usr_123");
const res = await request(app)
.get("/users/usr_123")
.set("Authorization", `Bearer ${token}`)
.expect(200)
.expect("Content-Type", /json/);
expect(res.body).toMatchObject({
id: "usr_123",
name: "Alice",
email: "alice@example.com",
});
expect(res.body).not.toHaveProperty("passwordHash");
});
it("returns 401 when token is missing", async () => {
const res = await request(app).get("/users/usr_123").expect(401);
expect(res.body.error).toBe("Unauthorized");
});
it("returns 404 when user does not exist", async () => {
const token = generateTestToken("usr_123");
await request(app)
.get("/users/nonexistent")
.set("Authorization", `Bearer ${token}`)
.expect(404);
});
});
Testing Error Responses
Error responses should be consistent. Define an error shape and test it:
describe("POST /users — validation", () => {
it("returns 422 when email is missing", async () => {
const res = await request(app)
.post("/users")
.set("Authorization", `Bearer ${adminToken}`)
.send({ name: "Bob" }) // missing email
.expect(422);
expect(res.body).toMatchObject({
error: "Validation failed",
details: expect.arrayContaining([
expect.objectContaining({ field: "email", message: expect.any(String) }),
]),
});
});
it("returns 409 when email already exists", async () => {
await request(app)
.post("/users")
.send({ name: "Alice", email: "alice@example.com" })
.set("Authorization", `Bearer ${adminToken}`);
// Second request with same email
const res = await request(app)
.post("/users")
.send({ name: "Alice2", email: "alice@example.com" })
.set("Authorization", `Bearer ${adminToken}`)
.expect(409);
expect(res.body.error).toMatch(/already exists/i);
});
});
Contract Testing with OpenAPI
Contract tests verify your implementation matches the API spec:
# openapi.yaml
paths:
/users/{id}:
get:
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
type: object
required: [id, name, email, createdAt]
properties:
id:
type: string
name:
type: string
email:
type: string
format: email
createdAt:
type: string
format: date-time
Use ajv or zod to validate response shapes against the schema in tests.
Common API Bugs to Check
| Bug | How to Test |
|---|---|
| Missing auth check on protected routes | Send request without token, expect 401 |
| Exposing internal fields (passwordHash, etc.) | Check response does not include sensitive keys |
| Incorrect HTTP method (GET modifying data) | Ensure GET is idempotent |
| Missing rate limit headers | Check X-RateLimit-* headers are present |
| Inconsistent error format | Compare error shape across endpoints |
| N+1 queries | Log query count per request in tests |
| Missing pagination | Request large collection, check Link header or cursor field |
Tools
- HTTP Request Builder — build and test HTTP requests visually
- JSON Diff Tool — compare API response bodies
- JWT Decoder — inspect authorization tokens during debugging