5 API Testing Mistakes That Waste Your Time
Testing APIs isn't glamorous, but it's where your backend's reliability is proven. As of February 2026, most teams still make these same testing mistakes that slow down development and hide bugs. Here's how to fix them.
1. Testing Only the Happy Path
Most API tests verify success responses. But your users hit errors, timeouts, and edge cases every day.
// ❌ Only testing what works
test('getUser returns user', async () => {
const res = await request(app).get('/api/users/1');
expect(res.status).toBe(200);
});
// ✅ Test the failures too
test('getUser returns 404 for missing user', async () => {
const res = await request(app).get('/api/users/999999');
expect(res.status).toBe(404);
expect(res.body.error).toMatch(/not found/i);
});
Fix: Aim for at least 30% of tests covering error scenarios.
2. No Test Data Isolation
Tests that share database state will flake. One failed test breaks everything else.
// ❌ Shared state = flaky tests
beforeAll(() => {
db.users.create({ id: 1, name: 'Test' });
});
afterAll(() => {
db.users.delete(1); // Might run before other tests finish
});
// ✅ Use transactions or fresh fixtures
test('createUser works', async () => {
const trx = await db.transaction();
const user = await createUser({ name: 'New' }, trx);
expect(user.id).toBeDefined();
await trx.rollback(); // Clean automatically
});
Fix: Use database transactions, in-memory DBs, or factory functions that create fresh data per test.
3. Ignoring Response Time Assertions
A 200 response doesn't mean your API is fast. Slow endpoints ruin user experience.
// ✅ Add timing checks
test('getUsers responds within 200ms', async () => {
const start = Date.now();
const res = await request(app).get('/api/users');
const duration = Date.now() - start;
expect(res.status).toBe(200);
expect(duration).toBeLessThan(200);
});
Fix: Add at least one timing assertion per endpoint. Track trends over time.
4. Not Testing Headers and Status Codes
Many tests only check the response body. But status codes and headers matter for caching, authentication, and content negotiation.
// ✅ Test the full response
test('getUser returns correct headers', async () => {
const res = await request(app)
.get('/api/users/1')
.set('Accept', 'application/json');
expect(res.status).toBe(200);
expect(res.headers['content-type']).toMatch(/json/);
expect(res.headers['cache-control']).toBeDefined();
});
Fix: Assert on status codes, content-type, caching headers, and rate-limit headers.
5. Manual Setup in Before/After
Complex setup logic hidden in before/after hooks makes tests unreadable and hard to debug.
// ❌ Magic setup hidden elsewhere
beforeEach(async () => {
await setupDatabase();
await seedUsers();
await seedProducts();
// ... 50 more lines
});
test('order creation', () => { ... });
// ✅ Self-documenting with test factories
test('order creation', async () => {
const user = await UserFactory.create();
const product = await ProductFactory.create();
const res = await request(app)
.post('/api/orders')
.send({ userId: user.id, productId: product.id });
expect(res.status).toBe(201);
});
Fix: Use test factories or builders that make setup explicit within each test.
Quick Wins Checklist
- [ ] Add error case tests (aim for 30%+)
- [ ] Isolate test data with transactions
- [ ] Add response time assertions
- [ ] Test headers and status codes
- [ ] Use factories instead of shared setup
These fixes take 30 minutes to implement but will catch bugs before they reach production. Your future self will thank you.
What API testing patterns have saved your team time? Drop them in the comments.
Top comments (0)