How to Unit Test an Express API in Node.js

How to Unit Test an Express API in Node.js

Ever deployed an API only to have it break unexpectedly in production?
Few things are more frustrating—and costly—than bugs that slip through because your code wasn’t properly tested.

That’s where unit testing comes in.

In this guide, I’ll walk you through how to unit test an Express API in Node.js, using practical examples from a Task Manager project. You’ll learn what to test, how to test it, and how to build confidence in your code before it reaches production.

🚀 Complete JavaScript Guide (Beginner + Advanced)

🚀 NodeJS – The Complete Guide (MVC, REST APIs, GraphQL, Deno)


What Is Unit Testing?

Unit testing is a software testing technique where individual units of code—such as functions, methods, or classes—are tested in isolation.

These tests are usually:

  • Automated
  • Fast
  • Focused on small pieces of logic

The goal is simple:
Verify that each unit behaves exactly as expected for a given input.

Why unit testing matters

  • Catches bugs early
  • Makes refactoring safer
  • Improves long-term code reliability
  • Gives you confidence to ship changes

Start With the Low-Hanging Fruit: Utility Functions

The best place to begin your unit testing journey is with utility functions.

Most utility functions are pure functions, meaning:

  • Same input → same output
  • No side effects
  • No dependency on external state (DB, UI, environment, etc.)

Because of this, they’re incredibly easy to test.

Example: getErrorMessage

This function extracts a meaningful message from different kinds of input:

  • An Error instance
  • An object with a message property
  • A string

Test cases you should cover:

  • Returns message from an Error object
  • Returns message from an object with a message property
  • Returns message from a string

Using Jest, this looks like:

  • describe() to group tests
  • test() for individual scenarios
  • expect() with matchers for assertions

If you’re new to Jest, spend time with the documentation—expect and matchers will become your daily tools.


Testing Functions With Dependencies

Not all functions are pure. Some rely on frameworks like Express.

For example, a utility like getPaginationParams() depends on the Express request object.
To test it properly, we simulate that dependency.

What to test

  • Default pagination values when page and perPage are missing
  • Correct parsing when valid query parameters are provided

How to test it

  1. Create a mock request object
  2. Cast it to Request (sometimes via unknown first in TypeScript)
  3. Pass it into your function
  4. Assert the returned pagination values

This lets you test real behavior without running an actual server.


Testing Classes and Business Logic

Next, move beyond functions and test classes—especially ones that contain business rules.

In the Task Manager project, we test a Task entity that handles:

  • Completion logic
  • Priority calculation
  • DTO transformation

What to test in a class

1. Initialization

Make sure a new instance is created with the correct default values.

2. State changes

Example:
When calling markAsCompleted():

  • completedOn should change from null to a Date instance.

Use:

expect(task.completedOn).toBeInstanceOf(Date);

This is safer than checking an exact timestamp.

3. Error handling

If a task is already completed, calling markAsCompleted() again should throw an error.
Test that an error is thrown, not necessarily the exact message—this makes your tests more resilient to future changes.

4. Business rules

Test logic like:

  • Priority is high when due date is within 1 day
  • Priority is low when due date is a week away
  • Priority is null when no due date exists

These are exactly the kinds of rules unit tests should protect.


Testing DTOs (Data Transfer Objects)

Your API responses are contracts with your users.
Unit tests help you ensure those contracts stay intact.

For example:

  • Internal fields like createdAt should not appear in the response
  • Computed fields like priorityLevel should be included

Test this by:

  1. Creating a DTO from the entity
  2. Asserting excluded fields are missing
  3. Asserting required fields are present

Running Your Tests

Once your tests are in place:

npm run test

To generate a coverage report:

npm run test -- --coverage

⚠️ Don’t forget the two dashes — they pass the flag correctly to Jest.


How Much Coverage Is Enough?

Coverage is useful—but it’s not everything.

Some teams require:

  • 70%
  • 80%
  • Or even 90% coverage

But remember:

High coverage doesn’t guarantee good tests.

A better rule of thumb

Test the code that contains:

  • Business logic
  • Decision-making
  • State changes

If a piece of code has no logic, it may not need a test at all.


What’s Next?

So far, we’ve focused on:

  • Pure functions
  • Simple dependencies
  • Entities and business logic

But real-world systems often include:

  • External services
  • Databases
  • Complex module interactions

These require mocking, spies, and advanced testing patterns—which I cover in this article.


Final Thoughts

Unit testing isn’t just about avoiding bugs.
It’s about building confidence in your codebase.

When your APIs are backed by solid tests, you can:

  • Refactor without fear
  • Ship faster
  • Sleep better at night

If you’re building Express APIs in Node.js, unit testing isn’t optional—it’s essential.

Share this article

Similar Posts