API testing with Jest and Supertest: backend test automation that catches real bugs
The Jest and Supertest setup we default to for API test automation, the three mistakes that turn a green backend suite into a misleading one, and how to run API tests in CI/CD.
Frontend test suites get all the attention because they're visible. API testing is what actually catches the bugs that reach production, because it runs closer to where the business logic lives. It's also the part of backend test automation we see teams get subtly wrong most often.
This is the Jest and Supertest setup we use for REST API testing on Node.js services, the three mistakes that make a passing suite misleading, and how to run it all in a CI/CD pipeline.
What is API testing, and why automate it?
API testing checks a service through its HTTP interface, without a browser: send a request, then check the status, the response body and what changed in the database. API test automation runs those checks on every change.
It earns its place because API tests are:
- Fast. No browser to start, so hundreds run in the time of a few end-to-end tests.
- Stable. No layout or timing to go wrong.
- Close to the rules. Validation, permissions and money calculations live in the backend, and that is where they should be tested.
The shape that holds up
Jest for the runner and assertions, Supertest for the HTTP layer, and a real (not mocked) database in a container:
import request from "supertest"
import { app } from "../src/app"
import { resetDb } from "./helpers/db"
beforeEach(async () => {
await resetDb()
})
describe("POST /orders", () => {
it("rejects an order with no line items", async () => {
const res = await request(app)
.post("/orders")
.send({ customerId: "cust_1", items: [] })
expect(res.status).toBe(422)
expect(res.body.error).toMatch(/at least one item/i)
})
})Nothing exotic. The value is in what it's not doing.
Mistake one: mocking the ORM
Mocking the database layer makes tests fast and makes them lie. The bug that actually reaches production is usually a real constraint: a unique index, a cascade delete, a transaction that isn't atomic when you thought it was. None of those exist in a mock. A real Postgres container that resets between tests costs a few seconds per run and catches the bugs that matter.
This is the difference between unit testing and integration testing. Both are useful. Only one of them tells you the service works.
Mistake two: asserting on status codes alone
expect(res.status).toBe(200) tells you the request didn't crash. It
doesn't tell you the response is right. We've seen a refactor silently drop
a field from every response body while the entire suite stayed green,
because every assertion stopped at the status code. Assert the shape:
expect(res.body).toMatchObject({
id: expect.any(String),
status: "pending",
total: 4200,
})Mistake three: sharing state across tests for speed
Reusing one seeded user across the whole file to save setup time is how you
get a suite where test order matters and nobody notices until someone runs
--runInBand versus parallel and gets different results. Every test seeds
and tears down its own data. It's slower per test and dramatically faster in
practice, because nobody burns an afternoon on a failure that only reproduces
in one execution order.
Shared state is also the usual cause of flaky tests. When we rebuilt one inherited suite, isolating test data was what made parallel runs possible.
The one Supertest habit worth adopting
Wrap auth in a helper that returns a ready request, not a token you attach manually every time:
const asAdmin = () => request(app).set("Authorization", `Bearer ${adminToken}`)Every test that forgets to attach a header is a test that's silently checking your 401 handling instead of the thing it claims to test. Centralize it once and that entire class of false positive disappears.
What an API test suite should cover
A suite that only tests the happy path is the same lie as a status-code assertion, one level up. For each endpoint we check:
- Validation: missing fields, wrong types, values out of range.
- Authentication and permissions: no token, an expired token, and a valid user who is not allowed to do this.
- Business rules: the calculations and limits the endpoint exists for.
- Error responses: the right status and a message a client can act on.
- Side effects: what was written to the database, and what was not.
- Repeat requests: sending the same request twice does not create two orders.
Running API tests in CI/CD
API tests are cheap enough to run on every pull request, and that is where they pay off. The setup is the same in GitHub Actions, Jenkins or Azure DevOps:
- Start the database as a container in the pipeline, the same image you use locally.
- Run migrations, so the schema under test is the one you will ship.
- Run the suite in parallel. This only works if every test owns its data, which is mistake three again.
- Fail the build on any failure. A suite that is allowed to fail stops being read.
Jest and Supertest, or another API testing tool?
The principles above matter more than the tool, and we use several depending on the stack:
- Jest and Supertest fit Node.js services, because the tests live in the same repository and language as the code.
- REST Assured is the equivalent choice for Java services.
- Playwright's API testing suits teams who want API and browser tests in one project. Playwright 1.64 added cookie control for exactly this.
- Postman is good for exploring an API by hand and sharing examples. We would not make it the only automated gate.
Where API tests fit with end-to-end tests
API tests do not replace browser tests. They let you keep the browser suite small. Check the rules through the API, where it is fast, and keep end-to-end tests for the journeys a customer actually takes: sign in, buy, pay. That balance is what keeps a regression run in minutes instead of hours.
Frequently asked questions
- What is API testing?
- API testing checks a backend service through its HTTP interface without using a browser. A test sends a request and then checks the status code, the response body and what changed in the database.
- Is Jest and Supertest good for API testing?
- Yes, for Node.js services. Jest provides the test runner and assertions and Supertest sends HTTP requests to the application, so the tests live in the same repository and language as the code they test.
- Should API tests use a real database or mocks?
- Use a real database in a container for API tests. The bugs that reach production usually involve real constraints such as unique indexes, cascade deletes and transactions, and none of those exist in a mock.
- How do you run API tests in a CI/CD pipeline?
- Start the database as a container in the pipeline, run the migrations, run the tests in parallel and fail the build on any failure. This works the same way in GitHub Actions, Jenkins and Azure DevOps.
- What is the difference between API testing and end-to-end testing?
- API testing checks the backend directly and is fast and stable. End-to-end testing drives the application through a browser the way a customer would. A good suite checks business rules through the API and keeps end-to-end tests for the main customer journeys.
Need an API or end-to-end suite your team can trust? Talk to us about test automation for your service, or start with a free test run on your web application.