Going ProfessionalPro· 40 min read

Testing Your API (Jest + Supertest)

Stop testing by clicking around. Write automated tests that hit your endpoints and prove they still work — every time, in seconds.

What you will learn

  • Explain why automated API tests matter
  • Write an integration test for an endpoint with Supertest
  • Assert on status codes and response bodies

Why automate testing?

So far you have tested your API by hand — opening Postman, clicking, eyeballing the result. That works for one endpoint, but the moment you have ten routes and change one, you would have to re-click all of them to be sure you broke nothing. Nobody does that reliably. Automated tests are little programs that call your endpoints and check the answers for you, in seconds, every time you run them. When a test that used to pass suddenly fails, you have caught a regression (a change that broke something that used to work) before your users do.

We use two tools together:

  • Jest — a test runner: it finds your test files, runs them, and reports pass/fail. (Vitest is a near-identical modern alternative; the syntax below works in both.)
  • Supertest — sends fake HTTP requests straight into your Express app (no real network, no port), so you can check the response.
Terminal: install test tools as dev dependencies
npm install --save-dev jest supertest

Note: Output: added a few packages in 2s --save-dev records these under devDependencies because they are only needed while developing, never in production. Jest runs the tests; Supertest drives requests into your app.

One small change makes testing possible

To test the app without actually opening a port, split building the app from starting it. Put your routes on app and export it; only call app.listen(...) in a separate entry file. Then a test can import the app directly.

app.js — exports the app, does not start it
// app.js — build the app and EXPORT it (no listen here)
const express = require("express");
const app = express();
app.use(express.json());

app.get("/tasks", (req, res) => res.json([{ id: 1, text: "Learn testing" }]));

module.exports = app;

Note: Output: (no output — this just exports the app) Notice there is no app.listen here. A separate server.js would do require("./app").listen(3000) to run it for real. Tests import app and never need a live port.

Writing your first test

A test file ends in .test.js. Inside, test(name, fn) describes one case, and expect(value).toBe(...) checks a result. Supertest’s request(app).get("/tasks") sends a fake GET request and lets you inspect the response:

An integration test for the /tasks endpoint
// app.test.js
const request = require("supertest");
const app = require("./app");

test("GET /tasks returns a 200 and a list", async () => {
  const res = await request(app).get("/tasks");

  expect(res.statusCode).toBe(200);
  expect(Array.isArray(res.body)).toBe(true);
  expect(res.body[0].text).toBe("Learn testing");
});

Reading it: request(app).get("/tasks") sends the request into your app and awaits the response. Then three checks (called assertions): the status code is 200, the body is an array, and the first item’s text is what we expect. If reality matches all three, the test passes; if any differs, Jest fails that line and shows what it got instead.

Terminal: run the tests
npx jest
# or add  "test": "jest"  to package.json scripts and run: npm test

Note: Output: PASS ./app.test.js ✓ GET /tasks returns a 200 and a list (35 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Jest found the .test.js file, ran the test, and reported a green pass. Now imagine running this after every change — you would know in one second whether you broke the /tasks route.

Testing a POST and a failure case

Good test suites check both the happy path and the failures. Supertest can send a JSON body, and you should assert that bad input is rejected:

Asserting that invalid input is rejected
test("POST /tasks rejects an empty body with 400", async () => {
  const res = await request(app)
    .post("/tasks")
    .send({});                    // no text — should fail validation

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

Note: Output: ✓ POST /tasks rejects an empty body with 400 This proves your validation actually works — and, just as importantly, it will keep proving it forever. If someone later removes the validation by accident, this test goes red immediately.

Tip: For routes that touch a database, point your tests at a separate test database (a different MONGO_URI for the test environment) so tests never pollute or wipe your real data. A common pattern is to clear the test database before each test so every test starts from a known, clean state.

Q. What does Supertest let you do in a test?

Answer: Supertest drives requests directly into your exported app without opening a port, so you can automatically check each endpoint’s status code and response body with Jest assertions.

✍️ Practice

  1. Split your API into app.js (exports the app) and server.js (listens), then write a test that asserts GET /tasks returns 200 and an array.
  2. Write a test that POSTs an invalid body and asserts a 400 response.

🏠 Homework

  1. Add a "test": "jest" script and write three tests for your CRUD API: list returns 200, creating with valid data returns 201, and creating with missing data returns 400.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →