How to Write API Integration Tests

How to Write API Integration Tests

Integration tests earn their value at the boundaries where unit tests stop telling the truth about production behavior. That makes them less about coverage counts and more about proving that real workflows survive real system constraints.

Where API Failures Escape Unit Tests

An API can have excellent unit test coverage and still fail in production.

Endpoints pass happy-path assertions. Services are mocked cleanly. CI stays green. Then the real system starts behaving differently:

  • duplicate requests create duplicate writes
  • auth rules fail for one role or tenant boundary
  • schema changes break older clients
  • retries expose race conditions that local tests never exercised
  • a database transaction commits only part of the expected state

These failures are rarely caused by a complete absence of tests. More often, they happen because the tests validated code in isolation while production depends on behavior across boundaries.

That is where API integration tests matter.


What API Integration Tests Are Actually For

API integration tests are not meant to replace unit tests. They exist to verify that the important parts of a request actually work together:

  • HTTP routing and request parsing
  • authentication and authorization
  • input validation
  • database reads and writes
  • transaction boundaries
  • serialization and response shape
  • interactions with queues, caches, or external services

The goal is not to test every branch through the API layer. The goal is to catch bugs that only appear when multiple components are wired together in a realistic way.

If unit tests protect local correctness, integration tests protect system behavior.


The Common Mistake

Many teams say they have API integration tests, but what they actually have is one of two weaker patterns:

  • controller tests with most dependencies mocked
  • end-to-end smoke tests that cover a few paths but explain little when they fail

The first misses real boundary problems. The second is often too slow and too broad to guide design.

A useful API integration test sits between them. It should exercise the real request path and real persistence layer while controlling only the dependencies that are genuinely outside the system boundary.

For most backend teams, that means:

  • run against a real test database
  • execute the real HTTP handler or application server
  • use real auth middleware
  • stub only third-party systems such as payment providers, email services, or other external APIs

What Good API Integration Tests Should Verify

A strong API integration test usually validates one or more of these concerns.

1. The request creates the correct persistent state

Do not stop at 200 OK or 201 Created. Check what was written.

If POST /orders succeeds, confirm:

  • the row exists in the database
  • related records were created correctly
  • default fields were populated as expected
  • invalid partial state was not left behind

Many production bugs live behind successful status codes.

2. The API enforces real authorization rules

Auth bugs often survive unit tests because permissions are split across middleware, handlers, and data access rules.

Integration tests should prove:

  • one user can access their own resource
  • another user cannot
  • tenant boundaries are enforced
  • role-based actions are correctly allowed or rejected

This is especially important for admin actions, billing flows, and any endpoint that mutates shared state.

3. The response contract matches what clients depend on

A passing test that checks only status code misses compatibility regressions.

Validate:

  • field presence
  • nullability expectations
  • enum values
  • pagination shape
  • error response structure

If you evolve APIs over time, this should connect directly to your compatibility strategy. That is closely related to the concerns in API Versioning Without Breaking Clients.

4. Failure paths leave the system in a safe state

This is where many API test suites are weak.

You should verify what happens when:

  • the database operation fails halfway through
  • a downstream dependency times out
  • validation passes but business rules reject the action
  • duplicate requests arrive with the same idempotency key

Production reliability depends on these cases more than on clean demos.

This is also where many teams discover the gap described in Why Tests Pass but Production Still Breaks.

Schema changes belong in this category too. If the application is reading and writing across old and new database shapes during a rollout, integration tests should prove that mixed-state data still behaves correctly. That rollout pattern is covered in Safe Database Migrations in Production.

5. Concurrency does not break correctness

Some of the highest-value API integration tests involve two requests, not one.

Examples:

  • two clients attempt the same reservation at the same time
  • two retries hit the same payment endpoint concurrently
  • two updates race on the same record

These are not exotic scenarios. They are ordinary production behavior once traffic and retries exist.

If your API supports safe retries, your tests should reflect the patterns discussed in Idempotency Keys for Duplicate API Requests.


A Practical Example

Imagine an endpoint:

POST /api/orders

The request should:

  • require authentication
  • validate item availability
  • create an order row
  • create order items
  • publish an event after commit

A useful integration test would verify more than the returned JSON.

it('creates an order and persists related state', async () => {
  const user = await createUser();
  const product = await createProduct({ stock: 3 });

  const response = await request(app)
    .post('/api/orders')
    .set('Authorization', `Bearer ${signToken(user)}`)
    .send({
      items: [{ productId: product.id, quantity: 2 }],
    });

  expect(response.status).toBe(201);
  expect(response.body.status).toBe('created');

  const order = await db.order.findFirst({
    where: { userId: user.id },
    include: { items: true },
  });

  expect(order).toBeTruthy();
  expect(order?.items).toHaveLength(1);
  expect(order?.items[0].quantity).toBe(2);

  const updatedProduct = await db.product.findUnique({
    where: { id: product.id },
  });

  expect(updatedProduct?.stock).toBe(1);
});

This test validates:

  • HTTP behavior
  • auth integration
  • database write correctness
  • related entity updates

That is already much closer to production than a mocked controller test.


The Highest-Value Test Cases Most Teams Skip

If you want API integration tests that catch real production bugs, prioritize these cases first.

Duplicate request handling

For write endpoints, verify that the same idempotency key does not create two side effects.

This is critical for:

  • payments
  • order creation
  • subscription changes
  • webhook consumers

Transaction rollback behavior

If part of the operation fails, prove that the database does not keep partial writes.

This catches:

  • missing transaction boundaries
  • out-of-order side effects
  • hidden coupling between repositories

Authorization edge cases

Test users with different roles, organizations, and ownership relationships.

A surprising number of severe bugs are simple permission gaps that never appeared in happy-path tests.

Compatibility checks for existing clients

If the endpoint changed recently, add tests that ensure older request or response expectations still work where promised.

Query-count or performance-sensitive paths

For hot endpoints, integration tests can enforce rough guardrails such as:

  • query count does not explode
  • pagination works predictably
  • large fixtures do not trigger obvious N+1 behavior

This is not full performance testing, but it can catch regressions early. It complements the database-focused issues covered in N+1 Query Problem in ORMs and How to Find and Fix Slow SQL Queries in Production.


What Not to Mock

If you mock too aggressively, integration tests collapse back into unit tests.

Avoid mocking:

  • your database layer
  • authentication middleware
  • serialization code
  • transaction handling
  • repository code that is part of your application boundary

Mock only what is outside your system boundary or too unstable to call directly in tests:

  • third-party APIs
  • email providers
  • SMS gateways
  • payment processors
  • external event consumers

The rule is simple: mock external dependencies, not your own core behavior.


Common Mistakes That Make Integration Tests Look Better Than They Are

Using unrealistic fixtures

Tiny, clean datasets hide issues with ordering, nullability, duplicates, and authorization scope.

Asserting only status codes

A 201 without database verification often proves very little.

Running tests against a fake persistence layer

An in-memory substitute may behave differently from your real database in transactions, constraints, and query semantics.

Ignoring concurrency

Single-request tests miss races, duplicate writes, and retry behavior.

Testing too much in one scenario

If a test covers auth, validation, billing logic, side effects, and notifications all at once, failures become hard to diagnose.

Keep each integration test focused on one system behavior.


A Practical Testing Strategy for Backend Teams

A workable API testing pyramid often looks like this:

  • unit tests for local business logic
  • API integration tests for request boundaries and persistence correctness
  • a small number of end-to-end tests for full-system confidence

For integration tests specifically:

  1. Cover the highest-risk write endpoints first.
  2. Use a real test database.
  3. Verify persisted state, not just responses.
  4. Add failure-path and authorization cases early.
  5. Add concurrency or idempotency tests where duplicates are costly.
  6. Keep the suite fast enough to run in CI.

This approach catches a large class of real bugs without turning the test suite into a slow simulation of production.


How to Decide Whether a New Integration Test Is Worth Adding

A good candidate usually matches at least one of these conditions:

  • the endpoint mutates important business data
  • the logic spans multiple tables or services
  • authorization mistakes would be costly
  • retries or concurrency can create duplicate effects
  • the endpoint has broken in production before
  • multiple clients depend on response compatibility

If none of these are true, a unit test may be enough. If several are true, an integration test is usually the right investment.


Closing Reflection

API integration tests are valuable not because they increase coverage numbers.

They are valuable because they verify the place where production bugs usually live: between components, across boundaries, under assumptions that unit tests do not fully model.

The best integration tests do not try to copy production exactly. They focus on the contracts, state changes, and failure modes that matter most when the system is real.