The Ultimate Testing Setup for Node.js + TypeScript (Jest + Supertest)
If you’re building APIs with Node.js and TypeScript, testing is what gives you confidence to refactor and ship faster. In this guide, I’ll show you a practical setup using Jest, TypeScript, and Supertest—from unit tests to real HTTP integration tests.
🚀 Complete JavaScript Guide (Beginner + Advanced)
🚀 NodeJS – The Complete Guide (MVC, REST APIs, GraphQL, Deno)
1. Project Setup
Initialize your project:
npm init -y npm install express npm install -D typescript ts-node @types/node npm install -D jest ts-jest @types/jest
Create a tsconfig.json:
{
"extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
Initialize Jest:
npx ts-jest config:init
jest.config.mjs:
export default {
preset: "ts-jest",
testEnvironment: "node",
roots: ["<rootDir>/src"]
};
Add scripts to package.json:
export default {
preset: "ts-jest",
testEnvironment: "node",
roots: ["<rootDir>/src"]
};
2. Example Utility + Unit Test
src/utils.ts
export function add(a: number, b: number) {
return a + b;
}
export function getErrorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === "string") return err;
return "Unknown error";
}
src/tests/utils.test.ts
import { add, getErrorMessage } from "../utils";
describe("utils", () => {
test("adds numbers", () => {
expect(add(2, 3)).toBe(5);
});
test("gets error message from Error", () => {
const err = new Error("Something went wrong");
expect(getErrorMessage(err)).toBe("Something went wrong");
});
test("gets error message from string", () => {
expect(getErrorMessage("Oops")).toBe("Oops");
});
});
Run tests:
npm test
3. Testing a Class with Logic
src/task.ts
export class Task {
name: string;
completedAt: Date | null = null;
constructor(name: string) {
this.name = name;
}
markComplete() {
if (this.completedAt) {
throw new Error("Already completed");
}
this.completedAt = new Date();
}
}
src/tests/task.test.ts
import { Task } from "../task";
describe("Task", () => {
test("marks task complete", () => {
const task = new Task("Test");
task.markComplete();
expect(task.completedAt).toBeInstanceOf(Date);
});
test("throws if already completed", () => {
const task = new Task("Test");
task.markComplete();
expect(() => task.markComplete()).toThrow();
});
});
4. Controller Test with Mocks
src/controller.ts
import { Request, Response } from "express";
export class TaskService {
async create(name: string) {
return { id: 1, name };
}
}
export async function createTask(req: Request, res: Response) {
const service = new TaskService();
const task = await service.create(req.body.name);
res.status(201).json({ task });
}
src/tests/controller.test.ts
import { createTask, TaskService } from "../controller";
jest.mock("../controller", () => {
const original = jest.requireActual("../controller");
return {
...original,
TaskService: jest.fn().mockImplementation(() => ({
create: jest.fn().mockResolvedValue({ id: 1, name: "Test" })
}))
};
});
test("creates task", async () => {
const req: any = { body: { name: "Test" } };
const json = jest.fn();
const status = jest.fn(() => ({ json }));
const res: any = { status };
await createTask(req, res);
expect(status).toHaveBeenCalledWith(201);
expect(json).toHaveBeenCalledWith({
task: { id: 1, name: "Test" }
});
});
5. Integration Tests with Supertest
Install:
npm install -D supertest @types/supertest
src/server.ts
import express from "express";
import { createTask } from "./controller";
export function createServer() {
const app = express();
app.use(express.json());
app.get("/health", (_, res) => {
res.json({ ok: true });
});
app.post("/tasks", createTask);
return app;
}
src/tests/integration.test.ts
import request from "supertest";
import { createServer } from "../server";
describe("API", () => {
test("health check", async () => {
const app = createServer();
const res = await request(app).get("/health");
expect(res.status).toBe(200);
expect(res.body.ok).toBe(true);
});
test("create task", async () => {
const app = createServer();
const res = await request(app)
.post("/tasks")
.send({ name: "New task" });
expect(res.status).toBe(201);
expect(res.body.task.name).toBe("New task");
});
});
6. Coverage
npm run test:coverage
Aim for strong coverage on logic-heavy code. Don’t chase 100%—focus on meaningful tests.
Final Thoughts
A solid testing strategy includes:
- Unit tests for logic
- Mocked collaboration tests
- Integration tests for routes
This combination gives you a codebase you can refactor without fear.
If you’re building Node.js APIs with TypeScript, investing in testing early will save you countless hours later.