API Testing

The process of verifying that APIs work correctly, securely, and perform well.

API testing verifies that your API works correctly before it reaches users. Unlike UI testing where you click buttons and fill forms, API tests interact directly with the backend through HTTP requests. This makes them faster to run, more reliable (no flaky browser issues), and they catch bugs closer to the source.

Why API Testing Matters

Frontend bugs are visible - a broken button is obvious. But API bugs are silent killers:

  • A validation bug might let invalid data into your database, corrupting months of records
  • A missing auth check could expose user data to anyone who guesses the endpoint
  • A slow query might work fine with 10 users but crash with 1000
  • An unhandled error might leak stack traces with sensitive info to attackers

The cost of finding these bugs increases 10x at each stage: development → testing → staging → production. API tests catch them early when they're cheap to fix.

Types of API Tests

Test TypeWhat It ChecksWhen It Fails
Contract TestingResponse structure matches schemaAPI returns {user_name} but frontend expects {userName}
Functional TestingBusiness logic works correctlyDiscount code applies 20% but should be 25%
Integration TestingServices work togetherPayment service can't talk to inventory service
Security TestingAuth, permissions, input sanitizationSQL injection in search field dumps entire database
Load TestingPerformance under stressSite crashes on Black Friday with 50k users

What to Test (Checklist)

For every endpoint, verify:

  • ✅ Returns correct status codes (200, 201, 400, 401, 404, 500)
  • ✅ Response body matches expected schema
  • ✅ Required fields are actually required
  • ✅ Invalid input returns helpful error messages
  • ✅ Auth tokens are validated (expired, invalid, missing)
  • ✅ Users can only access their own data
  • ✅ Pagination works correctly (first page, last page, empty results)
  • ✅ Response time is acceptable (set a threshold, e.g., <200ms)

Common Mistakes

1. Testing only happy paths Your API works perfectly when given perfect input. But users will send empty strings, null values, 10MB payloads, and SQL injection attempts. Test the weird stuff.

2. Hardcoded IDs in tests expect(user.id).toBe(42) will break when you reset the database. Test for existence (toHaveProperty('id')) or use dynamic values.

3. Not testing error responses A 500 error with a stack trace is a security risk. Verify your API returns proper error objects: {error: "Email already exists", code: "DUPLICATE_EMAIL"}

4. Skipping auth edge cases Does your API handle: expired tokens? tokens for deleted users? tokens with wrong permissions? most security breaches happen here.

5. Ignoring response times A test that passes in 3 seconds is a red flag. Add timeout assertions: expect(responseTime).toBeLessThan(200)

Best Practices

Structure tests with AAA pattern:

  • Arrange: Set up test data (create user, get auth token)
  • Act: Make the API call
  • Assert: Verify the response

Use test isolation: Each test should create its own data and clean up after. Never depend on data from other tests - they might run in parallel or different order.

Test at the right level:

  • Unit tests for business logic (fast, many)
  • Integration tests for API endpoints (medium speed, moderate count)
  • E2E tests for critical flows (slow, few)

Automate in CI/CD: Run API tests on every pull request. A broken API should never reach production.

Testing Tools

ToolBest ForLanguage
SupertestExpress/Node.js appsJavaScript/TypeScript
pytest + requestsPython APIsPython
REST ClientQuick manual tests in VS CodeAny
PostmanTeam collaboration, collectionsAny
k6Load testingJavaScript
HurlSimple HTTP test filesAny

Code Examples

API Test with Supertest

import request from 'supertest';
import app from './app';

describe('POST /api/users', () => {
  it('creates user with valid data', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: 'John', email: 'john@test.com' });

    expect(res.status).toBe(201);
    expect(res.body).toHaveProperty('id');
  });

  it('rejects invalid email', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: 'John', email: 'not-an-email' });

    expect(res.status).toBe(400);
    expect(res.body.error).toContain('email');
  });
});