In short
Every function can pass its unit tests and the app can still break, because the pieces don't fit. Integration tests check that your route, service and real database actually work together.
Testing how multiple backend pieces work together — a route handler plus its service plus a real (or test) database — closer to real usage than isolated unit tests.

Think of it like this
The assembled pressure cooker
The lid, gasket and whistle can each be perfect, and the cooker still leaks if the gasket doesn't sit right. You only find out by putting it together and cooking. Integration tests assemble the real parts, route plus database, and check the whistle blows.
Watch it happen
One test, real parts
TRUNCATE orders
1/5Before each test, the orders table is wiped so the test starts clean.
The big ideas
What you need to know
Real parts, wired together
Use the real router, services and a real database. Fake only things you don't own, like payment gateways.
A separate test database
Point tests at their own database, often in Docker, never at your development or production data.
Reset between tests
Truncate or roll back before each test, so one test's leftovers never break another.
Slower, so fewer
Integration tests take longer than unit tests. Cover the important paths, not every branch.
See it in code
Walk through the code
1import { beforeAll, beforeEach, afterAll, test, expect } from "vitest";2import { app } from "../src/app";3import { pool } from "../src/db";4 5let server: ReturnType<typeof app.listen>;6beforeAll(() => new Promise<void>((done) => { server = app.listen(0, () => done()); }));7beforeEach(() => pool.query("TRUNCATE orders RESTART IDENTITY"));8afterAll(async () => { server.close(); await pool.end(); });9 10test("POST /orders saves the order", async () => {11 const url = `http://localhost:${(server.address() as { port: number }).port}/orders`;12 const res = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ item: "chai", qty: 2 }) });13 expect(res.status).toBe(201);14 const { rows } = await pool.query("SELECT item, qty FROM orders");15 expect(rows).toEqual([{ item: "chai", qty: 2 }]);16});1/4Start the real app on a random free port before the tests run.
Point DATABASE_URL at a throwaway test database, for example a Postgres container started just for the tests.
Save yourself the pain
Common mistakes
Oops
Running integration tests against your shared development database.
Do this instead
Tests will wipe or depend on real data. Use a dedicated test database that can be reset freely.
Oops
Letting tests depend on running in a certain order.
Do this instead
Reset state before each test. Every test should pass when run on its own.
Your mission
Test a route end to end
Quick check
Unit tests pass, but POST /orders fails in production because of a wrong column name in the SQL. Which test would have caught it?
Go deeper
- 1Write a test that hits a real API endpoint backed by a test database.
- 2Set up and tear down test data cleanly between test runs.
- 3Notice the trade-off: integration tests give more confidence but run slower than unit tests.
More topics at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.