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": "[email protected]"}' \
  | 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: "[email protected]" } });
  });

  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: "[email protected]",
    });
    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: "[email protected]" })
      .set("Authorization", `Bearer ${adminToken}`);

    // Second request with same email
    const res = await request(app)
      .post("/users")
      .send({ name: "Alice2", email: "[email protected]" })
      .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

BugHow to Test
Missing auth check on protected routesSend 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 headersCheck X-RateLimit-* headers are present
Inconsistent error formatCompare error shape across endpoints
N+1 queriesLog query count per request in tests
Missing paginationRequest large collection, check Link header or cursor field

Tools