Test and operate APIs

Write an API security matrix

Test every route across identity, role, object relationship, input boundary, rate limit, and expected security event.

API security tests need more than one happy-path token. Build a matrix that changes one security condition at a time, so a failing cell points at exactly one broken control.

Choose the rows

Include anonymous, expired, revoked, low-privilege, wrong-tenant, and administrative identities. Test another user’s object, hidden properties, alternate methods, batch requests, oversized input, and repeated calls.

For one high-value route, the matrix reads like this:

route: POST /invoices/:id/refund
  anonymous              -> 401, no refund row
  expired token          -> 401, no refund row
  wrong tenant           -> 404, no refund row
  low-privilege role     -> 403, no refund row, denial event logged
  admin, valid           -> 200, one refund row, audit event logged
  admin, key replayed    -> 200 same body, still exactly one refund row

Each row is a real request with real credentials against a real database, not a mocked unit test. Authorization bugs live in the wiring between middleware, handler, and query — a mock skips exactly the layer you are trying to test.

Assert state, not just status

A 403 alone does not prove safety if the invoice changed before the response. The matrix must check identity, outcome, stored state, and the expected security event. Verify stored state and events, not only status codes:

const res = await api.post('/invoices/inv_9f3c2a/refund', body, wrongTenantToken)
assert.equal(res.status, 404)

const refunds = await db.query(
  'SELECT count(*) FROM refunds WHERE invoice_id = $1', ['inv_9f3c2a'])
assert.equal(refunds.rows[0].count, '0')   // no side effect happened

const events = await db.query(
  "SELECT count(*) FROM audit_events WHERE type = 'authz_denied'")
assert.equal(events.rows[0].count, '1')    // the denial left evidence

Prioritize by harm

Prioritize cells by potential harm instead of automating every permutation first. Cross-tenant writes and money movement deserve stronger evidence than a harmless malformed optional field. Forty high-value cells that run on every commit beat four thousand cells nobody maintains.

One extra step keeps the matrix honest: break a policy on purpose. Comment out one permission check locally and confirm the matching cell fails. A matrix that stays green through a real regression is measuring nothing.

Automate the highest-impact rows for two users, two tenants, an expired token, and an oversized body. Save assertions for response, database state, and audit event, then make one policy fail and prove the test catches it.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →