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.
API Testing
The process of verifying that APIs work correctly, securely, and perform well.
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 Type | What It Checks | When It Fails |
|---|---|---|
| Contract Testing | Response structure matches schema | API returns {user_name} but frontend expects {userName} |
| Functional Testing | Business logic works correctly | Discount code applies 20% but should be 25% |
| Integration Testing | Services work together | Payment service can't talk to inventory service |
| Security Testing | Auth, permissions, input sanitization | SQL injection in search field dumps entire database |
| Load Testing | Performance under stress | Site 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
| Tool | Best For | Language |
|---|---|---|
| Supertest | Express/Node.js apps | JavaScript/TypeScript |
| pytest + requests | Python APIs | Python |
| REST Client | Quick manual tests in VS Code | Any |
| Postman | Team collaboration, collections | Any |
| k6 | Load testing | JavaScript |
| Hurl | Simple HTTP test files | Any |
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');
});
});Related Terms
Idempotency
A property where making the same API request multiple times produces the same result.
JWT (JSON Web Token)
A compact, URL-safe token format for securely transmitting information between parties.
API Mocking
Simulating API behavior to enable development and testing without real backend services.
Service Virtualization
An enterprise-grade technique for simulating the behavior of dependent services, APIs, and systems that are unavailable or costly to access.