Claude CodeIntermediate

How to Add an Integration Test for an API Route with Claude Code

Have Claude Code write an end-to-end style test that hits a real route handler and asserts status, body, and side effects.

9 minIntermediate

Unit tests prove a function works in isolation; integration tests prove a route actually responds correctly when wired together. Claude Code can write a test that sends a request to your handler and checks the status, the response body, and any side effect like a database write. The key is to test through the real entry point, not a mocked stand-in.

What you need

  • Claude Code in your backend project
  • A test runner and an HTTP test helper (supertest, the framework's test client, or fetch against a test server)
  • One route you want covered end to end

Step 1: Point Claude at the route and its dependencies

Name the route file and tell Claude how the app is started in tests. If there is a test setup file or an existing integration test, point at it so the new test follows the same bootstrapping.

Claude Code
You
Write an integration test for POST /api/users in src/routes/users.ts. Follow the bootstrapping in test/setup.ts. Assert 201, the returned id, and that the user exists in the test database afterward.
Agent
Found supertest and an in-memory SQLite setup in test/setup.ts. I'll create test/users.int.test.ts that posts a valid payload, asserts 201 and the id, then queries the db to confirm the row.
Share the route and how the app boots in tests.

Step 2: Review the request and assertions

Read the generated test. Confirm the payload is realistic, the status assertion is correct, and the side-effect check actually queries the store rather than trusting the response alone.

test/users.int.test.ts
import request from "supertest";
import { app, db } from "./setup";

it("creates a user", async () => {
  const res = await request(app)
    .post("/api/users")
    .send({ email: "a@b.com", name: "Ada" });

  expect(res.status).toBe(201);
  expect(res.body.id).toBeDefined();

  const row = db.get("select * from users where id = ?", res.body.id);
  expect(row.email).toBe("a@b.com");
});

Step 3: Add the failure cases

A route test that only covers the happy path is weak. Ask Claude to add a rejection case, like a duplicate email or a missing field, and assert the correct error status.

test/users.int.test.ts
it("rejects a missing email with 400", async () => {
  const res = await request(app)
    .post("/api/users")
    .send({ name: "NoEmail" });

  expect(res.status).toBe(400);
});

Step 4: Run and watch for shared state

Run the integration suite. The most common failure here is leftover state between tests, so confirm each test resets the database. If tests pass alone but fail together, that is the cause.

zsh - my-app
$npx vitest run test/users.int.test.ts
Tests 2 passed (2)
db reset in beforeEach, no cross-test bleed
$
Reset state between tests
Ask Claude to add a beforeEach that clears the test database. Integration tests that share state pass and fail at random, which is worse than no test.

Result: a test that posts to the real route, checks the 201 and the persisted row, rejects a bad payload with 400, and runs cleanly thanks to a per-test database reset.

Watch related tutorials

Tags
#testing#integration#api