20 Dec 2024 Testing BFF APIs with Supertest and MSW
In modern web development, testing isn’t just a side task; it’s integral to delivering robust, high-performing applications.
In this post, I’ll explore how to effectively test a Backend for Frontend (BFF) API using two powerful tools: Supertest and Mock Service Worker (MSW). I’ll break down the step-by-step process of building a domain service and a BFF service, and then dive into how to write integration tests that use MSW to mock the domain service’s responses to ensure that your tests run smoothly without relying on the actual domain service.
What is a Domain Service?
A domain service is a backend service that encapsulates core business logic and data operations for a specific domain of your application.
In our example, the domain service is a REST API that handles the core functionality of fetching and managing shopping deals in an e-commerce application. It is responsible for:
- Implementing business rules
- Providing a clean API for accessing domain-specific operations
- Maintaining separation of concerns from presentation logic (which is defined in the BFF)
What is a BFF?
Before diving into the technicalities, let’s clear up what a BFF is all about.
Imagine this: your frontend needs data, but it doesn’t want everything from your backend, just the essentials—clean, tailored, and fast. This is where BFF comes into play. It’s a pattern where you create a backend service that’s tightly coupled to your frontend, designed to handle specific data transformations and optimisations.
A BFF essentially serves as a middleware between your frontend and various domain services. It takes in requests, fetches or aggregates data from different sources, transforms it into the desired structure, and sends it back to the frontend in a simplified format.
Real-World BFF Example
Consider an e-commerce application that needs to display product details, inventory status, pricing, and user reviews.
Without a BFF, our application interacts directly with each of the relevant domain services:
Frontend -> Product Service (product details)
-> Inventory Service (stock status)
-> Pricing Service (current price, discounts)
-> Reviews Service (user reviews)
When we introduce a BFF, it becomes an intermediary:
Frontend -> BFF -> Product Service (product details)
-> Inventory Service (stock status)
-> Pricing Service (current price, discounts)
-> Reviews Service (user reviews)
Specifically, the BFF can:
- Combine data from multiple services into a single response
- Remove unnecessary fields
- Transform data formats
- Implement client-specific optimizations
- Provide a secure buffer between the frontend and the domain services
Why Use Supertest and MSW?
Supertest is a fantastic library for testing HTTP servers in Node.js. It allows you to simulate API calls and assert against the responses. You can test API routes without needing the server to be fully up and running.
MSW shines when you want to simulate API responses. With MSW, you can intercept API calls and mock responses as if they were coming from your real backend, making your tests fast and resilient against external API failures.
For a better understanding, let’s look at the flow of API calls differs whether you’re using MSW or not.
Without MSW
Without MSW, the BFF server sends an API request to the Domain server. The response is returned from the Domain server without any interception, which means the BFF is dependent on the Domain server being up and running for the API request to succeed.
With MSW:
Now let’s see how Mock Service Worker (MSW) intercepts an API request made by the BFF (Backend for Frontend) server. Instead of the request reaching the Domain server, MSW provides a mocked response, enabling faster and isolated testing without relying on the actual backend service.
With that foundation, let’s jump right into the step-by-step guide to build the Domain service, BFF service, and integration tests using Supertest and MSW.
Step 1: Create the Domain Service (AZDeals-Domain)
In our example, we’ll focus on a domain service that fetches deals from an external service based on the country code provided by the frontend.
Here’s the setup:
- Create the Node.js project and install dependencies:
mkdir AZDeals-domain
cd AZDeals-domain
npm init -y
yarn add express node-fetch@2 dotenv
yarn add -D @types/express @types/node @types/node-fetch ts-node-dev typescript
- Build the Domain API (src/server.ts):
This API will accept a country code as a path parameter and return a list of deals. The response mimics fetching real-time deals from Amazon’s API.
To make this run, generate your own rapid api key by signing up on https://rapidapi.com/. Once you have the api key, store it in your .env file.
⚠️ Important: The examples below use environment variables for API keys. In a production environment, never store sensitive credentials in code repositories or .env files. Instead:
- Use a secure secrets management service (like AWS Secrets Manager, HashiCorp Vault)
- Implement proper key rotation
- Use instance roles/managed identities where possible
- Consider using API gateways with token-based authentication
// Import necessary modules
import express, { Request, Response } from "express";
import fetch from "node-fetch";
import dotenv from "dotenv"; // To load .env variables
dotenv.config();
// Create an express application
const app = express();
// Define the API endpoint with dynamic country code
app.get("/getDeals/:countryCode", async (req: Request, res: Response) => {
const { countryCode } = req.params; // Extract countryCode from path params
const url = `https://real-time-amazon-data.p.rapidapi.com/deals-v2?country=${countryCode}&min_product_star_rating=4&price_range=ALL&discount_range=ALL`;
const options: any = {
method: "GET",
headers: {
"x-rapidapi-key": process.env.RAPIDAPI_KEY as string, // Use your API key from .env
"x-rapidapi-host": "real-time-amazon-data.p.rapidapi.com",
},
};
try {
// Fetch data from the API
const response = await fetch(url, options);
if (response.ok) {
const result: any = await response.json(); // Parse the result as JSON
res.json(result); // Send the JSON result to the client
} else {
res
.status(response.status)
.json({ error: `API error: ${response.statusText}` });
}
} catch (error: any) {
// Handle any errors during the API call
console.error("Error fetching deals:", error);
res.status(500).json({ error: "Failed to fetch deals" });
}
});
// Define a port to run the server
const PORT = process.env.PORT || 3000;
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
You have successfully created your domain service. You can start this server using:
yarn ts-node-dev src/server.ts
Step 2: Create the BFF Service (AZDeals-BFF)
The BFF service will consume the Domain service, transforming its response into a frontend-friendly format. Here’s how you can set it up:
- Initialise a new Node.js project for the BFF service:
mkdir AZDeals-bff
cd AZDeals-bff
npm init -y
yarn add express node-fetch dotenv
yarn add -D @types/express @types/jest @types/node @types/node-fetch @types/supertest jest msw supertest ts-jest ts-node-dev typescript
- Build the BFF API (src/server.ts):
import express from "express";
import fetch from "node-fetch";
import dotenv from "dotenv"; // To load .env variables
dotenv.config();
// Create an express application
const app = express();
// Define the API endpoint
app.get("/getAustraliaDeals", async (req, res) => {
const domainApiUrl = "http://localhost:4444/getDeals/AU";
try {
const response = await fetch(domainApiUrl);
if (response.ok) {
const domainData = await response.json();
const deals = domainData.data.deals.map((deal: any) => ({
deal_title: deal.deal_title,
deal_photo: deal.deal_photo,
deal_url: deal.deal_url,
}));
res.json({ deals });
} else {
res
.status(response.status)
.json({ error: `Domain API error: ${response.statusText}` });
}
} catch (error) {
console.error("Error fetching Australia deals:", error);
res.status(500).json({ error: "Failed to fetch Australia deals" });
}
});
const PORT = process.env.PORT || 5000;
const server = app.listen(PORT, () => {
console.log(`AZDeals-bff server is running on port ${PORT}`);
});
// Export both `app` and `server`
export { app, server };
Step 3: Write Integration Tests Using Supertest and MSW
Now, the fun part—testing!
Following are the steps you can take to write the BFF integration tests using Supertest and MSW. Supertest will be used to assert the HTTP responses, and MSW will be used to mock the domain service so that the BFF tests can be tested without any external dependencies.
- Set up MSW (mocks/server.ts): This will initialise the MSW server to intercept API calls.
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
// Set up MSW to intercept network requests
export const server = setupServer(...handlers);
// Before all tests, start the server
beforeAll(() => server.listen());
// After each test, reset any request handlers
afterEach(() => server.resetHandlers());
// After all tests, close the server
afterAll(() => server.close());
- Set up MSW handlers (mocks/handlers.ts):
Now, let’s define a mock handler to simulate the API responses for the Domain service so that these tests can run in isolation.
import { HttpResponse, http } from 'msw';
export const handlers = [
http.get('http://localhost:4444/getDeals/AU', () => {
return HttpResponse.json({
"status": "OK",
"request_id": "eb12fe4a-3e8d-4987-b57b-ef4e67a3f4c7",
"parameters": {
"country": "AU",
"min_product_star_rating": "4",
"price_range": "ALL",
"discount_range": "ALL"
},
"data": {
"deals": [
{
"deal_id": "f43361da",
"deal_type": "BEST_DEAL",
"deal_title": "Shark FlexStyle Limited Edition Teal 5-in-1 Hair Styler & Dryer Gift Set, Curling Attachments, Finishing Attachment, Oval Brush, Concentrator, Hair Clip, Storage Bag, No Heat Damage",
"deal_photo": "https://m.media-amazon.com/images/I/71a8S7ARpdL.jpg",
"deal_state": "AVAILABLE",
"deal_url": "https://www.amazon.com.au/Shark-FlexStyle-Attachments-Attachment-Concentrator/dp/B0CKW59L1Y",
"canonical_deal_url": "https://www.amazon.com.au/deal/f43361da",
"deal_starts_at": "2024-09-04T22:00:00.000Z",
"deal_ends_at": "2024-09-17T21:55:00.000Z",
"deal_badge": "39% off",
"type": "SINGLE_ITEM",
"product_asin": "B0CKW59L1Y"
},
{
"deal_id": "64c4d7f2",
"deal_type": "BEST_DEAL",
"deal_title": "FOREO Luna 4 Body Body Brush | Exfoliating Body Scrubber | Enhances Absorption of Lotion Actives | Premium Lymphatic Drainage Skincare Tool | 100% Waterproof | USB-Rechargeable, Evergreen",
"deal_photo": "https://m.media-amazon.com/images/I/81R2C9JjEQL.jpg",
"deal_state": "AVAILABLE",
"deal_url": "https://www.amazon.com.au/FOREO-Exfoliating-Absorption-Waterproof-USB-Rechargeable/dp/B0BGL9PQVL",
"canonical_deal_url": "https://www.amazon.com.au/deal/64c4d7f2",
"deal_starts_at": "2024-09-08T14:00:00.000Z",
"deal_ends_at": "2024-09-15T13:55:00.000Z",
"deal_badge": "44% off",
"type": "SINGLE_ITEM",
"product_asin": "B0BGL9PQVL"
},
{
"deal_id": "84f28e11",
"deal_type": "BEST_DEAL",
"deal_title": "Meteor Essential Barbell Set - Entry Level Dumbbell Set Home Gym Dumbbell Weightlifting Weight Plates Dumbbell Barbell",
"deal_photo": "https://m.media-amazon.com/images/I/61pFcRthWSL.jpg",
"deal_state": "AVAILABLE",
"deal_url": "https://www.amazon.com.au/Meteor-Essential-40KG-Barbell-Weightlifting/dp/B0CV3S2CQB",
"canonical_deal_url": "https://www.amazon.com.au/deal/84f28e11",
"deal_starts_at": "2024-09-08T14:00:00.000Z",
"deal_ends_at": "2024-09-15T13:45:00.000Z",
"deal_badge": "Limited time deal",
"type": "SINGLE_ITEM",
"product_asin": "B0CV3S2CQB"
}
],
"total_deals": 3,
"country": "AU",
"domain": "www.amazon.com.au"
}
});
}),
];
- Write integration tests (test/bff.test.ts):
Finally, we write the test case using Supertest to hit the BFF endpoint and validate that it responds with the correct data.
import request from 'supertest';
import { app } from '../src/server';
import { server } from '../mocks/server'; // Import the MSW server
describe('BFF API Integration Test', () => {
it('should fetch Australia deals and transform the response correctly', async () => {
server.use(); // Start the MSW server
const response = await request(app).get('/getAustraliaDeals');
// Validate the response
expect(response.status).toBe(200);
expect(response.body).toEqual({
deals: [
{
deal_title: 'Shark FlexStyle Limited Edition Teal 5-in-1 Hair Styler & Dryer Gift Set, Curling Attachments, Finishing Attachment, Oval Brush, Concentrator, Hair Clip, Storage Bag, No Heat Damage',
deal_photo: 'https://m.media-amazon.com/images/I/71a8S7ARpdL.jpg',
deal_url: 'https://www.amazon.com.au/Shark-FlexStyle-Attachments-Attachment-Concentrator/dp/B0CKW59L1Y'
},
{
deal_title: 'FOREO Luna 4 Body Body Brush | Exfoliating Body Scrubber | Enhances Absorption of Lotion Actives | Premium Lymphatic Drainage Skincare Tool | 100% Waterproof | USB-Rechargeable, Evergreen',
deal_photo: 'https://m.media-amazon.com/images/I/81R2C9JjEQL.jpg',
deal_url: 'https://www.amazon.com.au/FOREO-Exfoliating-Absorption-Waterproof-USB-Rechargeable/dp/B0BGL9PQVL'
},
{
deal_title: 'Meteor Essential Barbell Set - Entry Level Dumbbell Set Home Gym Dumbbell Weightlifting Weight Plates Dumbbell Barbell',
deal_photo: 'https://m.media-amazon.com/images/I/61pFcRthWSL.jpg',
deal_url: 'https://www.amazon.com.au/Meteor-Essential-40KG-Barbell-Weightlifting/dp/B0CV3S2CQB'
}
]
});
});
// Add any other tests like error handling, etc.
});
- Some final configurations (in the project root):
- Set up jest config (jest.config.js):
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};
- Set up TS config (tsconfig.json):
{
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
- Your package.json file should look like this:
{
"name": "azdeals-bff",
"version": "1.0.0",
"description": "",
"main": "src/server.ts",
"scripts": {
"build": "npx tsc",
"start": "npx ts-node-dev src/server.ts",
"dev": "ts-node-dev src/server.ts",
"test": "jest --forceExit"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^10.0.0",
"express": "^4.18.1",
"node-fetch": "^2.6.1"
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/jest": "29.5.12",
"@types/node": "^18.7.14",
"@types/node-fetch": "^2.5.12",
"@types/supertest": "6.0.2",
"jest": "29.7.0",
"msw": "2.4.4",
"supertest": "7.0.0",
"ts-jest": "29.2.5",
"ts-node-dev": "^2.0.0",
"typescript": "^5.5.4"
}
}
If you’ve done everything right, your final project structure should look like this:
AZDeals-bff/
│
├── mocks/ # Mock Service Worker (MSW) setup
│ ├── handlers.ts # Defines mock handlers for API requests
│ └── server.ts # MSW server setup for tests
│
├── src/ # Source code for the BFF service
│ └── server.ts # BFF server logic (main API logic)
│
├── test/ # Integration tests folder
│ └── bff.test.ts # Supertest and MSW integration tests
│
├── jest.config.js # Jest configuration for testing
├── package.json # Project dependencies and scripts
└── tsconfig.json # TypeScript configuration
This project structure contains:
- mocks: This contains MSW setup files, including handlers for mocking API responses and the MSW server configuration.
- src: The main BFF server source code.
- test: Contains the integration tests.
- Other files like jest.config.js, package.json, and tsconfig.json for configuration and dependency management.
Now, just execute the defined script to run the tests:
yarn test
and the results should look like this:
PASS test/bff.test.ts
BFF API Integration Test
✓ should fetch Australia deals and transform the response correctly (73 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.507 s, estimated 2 s
Ran all test suites.
Conclusion
By setting up Supertest and MSW, we have created an efficient and reliable testing environment for your BFF API. MSW enables you to mock backend services like the Domain service, allowing you to test your BFF logic in isolation without depending on external services.
While we’ve demonstrated this testing approach with a BFF architecture, it’s important to note that the Supertest + MSW combination is incredibly versatile and can be applied to many different architectural patterns. For example:
- Microservices testing: Mock inter-service communication
- Gateway APIs: Test request routing and transformation logic
- Traditional monolithic applications: Mock external API dependencies
- Serverless functions: Test API integrations locally
- GraphQL resolvers: Mock downstream data sources
- OAuth/Authentication flows: Mock identity provider responses
The combination of Supertest for API testing and MSW for mocking backend responses ensures your tests are fast, reliable, and independent. By doing so, you eliminate any uncertainties around external service availability, making your tests more robust.
Resources
Source Code
The complete source code for this tutorial is available in two GitHub repositories:
Domain Service (AZDeals-domain)
- Contains the backend domain service implementation
- Includes the Amazon deals API integration
- Complete TypeScript/Express setup
- Contains the BFF service implementation
- Includes complete testing setup with Supertest and MSW
- Example integration tests
- Full TypeScript configuration
Feel free to clone these repositories to follow along with the tutorial or use them as a reference for implementing similar testing patterns in your own projects.
Happy coding and testing!
No Comments