A Practical Testing Pyramid for REST APIs
The pyramid we actually use
| Layer | Count | Speed | What it catches |
|---|---|---|---|
| Unit | 300+ | 2s | Logic errors, edge cases |
| Integration | 50 | 15s | DB queries, middleware wiring |
| Contract | 20 | 5s | API response shape changes |
| E2E | 5 | 3m | Deployment sanity (smoke only) |
The insight
We removed all but 5 E2E tests. The remaining ones just check that the server starts, serves a 200 on /health, and can complete a full write→read cycle. Everything else is covered by faster layers.
Contract testing with golden files
func TestAPIResponseShape(t *testing.T) {
resp := testServer.Get(t, "/api/users/1")
golden.Assert(t, resp.Body, "testdata/get-user.golden.json")
}
When the API changes, update the golden file. When a PR forgets, the test fails.
What I learned
The traditional testing pyramid recommends many unit tests, fewer integration, fewer E2E. What it doesn’t say: E2E tests should be smoke tests, not comprehensive coverage. Comprehensive E2E is a maintenance sink.