Node.js Microservice Architecture Organizing and testing HTTP requests
Node.js Part 13 May 24, 2022
We want to send HTTP requests to other services. On production, we should contact the services. Locally and in tests, we want to contact mocks instead.
We assume the existence of a notification service. If a todo is deleted, then a notification should be created in that notification service. The notification service offers an API endpoint POST /notifications
to do that.
We install the library axios
to perform HTTP requests.
npm install --save axios
npm install --save-dev @types/axios
Sending an HTTP request 🔗
We create a module controllers/todos/notification.service.ts
. Similarly to the data access module controllers/todos/todo.dao
, it contains the precise queries we want to perform in the context of the todo controllers. It's not the goal to build a full TypeScript representation of the complete API of the notification service. Instead, the notification.service
contains only the required function sendNotification
.
import axios from "axios";
import { config } from "../../configuration/config";
export function sendNotification(message: string): Promise<void> {
return axios.post(`${config.http.servicesUrl}/notify/notifications`, {
message,
});
}
The API endpoint POST /notifications
provides more options. But we only implement the options that are required in the todo service. This avoids complexity and makes testing easier.
Configuring the services URL 🔗
In the configuration configuration/config.ts
, we put the URL servicesUrl
.
import { Knex } from "knex";
import { Level } from "pino";
export type Environment =
// The service running in a production cluster available for customers
| "production"
// The service running locally on a development machine
| "local"
// The service running with Jest executing tests
| "test";
export interface Config {
// ...
http: {
servicesUrl: string;
};
}
export interface ProcessVariables {
// ...
SERVICES_URL?: string;
}
On the production environment, we read the URL from an environment variable.
import { Config, ProcessVariables } from "../config.type";
export function getProductionConfig(
processVariables: ProcessVariables
): Config {
return {
// ...
http: {
servicesUrl:
processVariables.SERVICES_URL ??
"<SERVICES_URL> needs to bet set in production environment",
},
};
}
Locally, we set the URL to port 3001
on localhost
.
import { Config, ProcessVariables } from "../config.type";
export function getLocalConfig(processVariables: ProcessVariables): Config {
return {
// ...
http: {
servicesUrl: "http://localhost:3001",
},
};
}
At this local URL http://localhost:3001
, we spin up a server that responds with mock responses.
Note: In my experience, it also works well to connect for local development with a production environment with test workspaces. This has the advantage of getting responses of actual production services without the overhead of running these other services locally. However, it also makes it easy to create a lot of coupling between services.
Mocking responses in local development 🔗
There are several options for mocking HTTP responses locally. We choose the npm package mockserver
. It allows to organize the responses with the file system structure.
We create a file mocks/notify/notifications/POST.mock
. The package mockserver
returns this response on a call to the route POST /notify/notifications
.
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"message": "Notification created"
}
In the package.json
, we add a command start:mocks
to start the mock server on http://localhost:3001
with the folder mocks
. The npm package concurrently
allows us to run the app, the TypeScript compiler and the mock server in parallel.
{
// ...
"scripts": {
// ...
"start": "npm run build && npx concurrently \"npm:start:*\"",
"start:app": "nodemon --legacy-watch --watch ./dist --inspect=0.0.0.0:9229 dist/main.js -- server | npx pino-pretty -i time,hostname,module,__in,name,pid",
"start:ts": "tsc --watch --incremental",
"start:mocks": "npx mockserver -p 3001 -m mocks"
// ...
}
// ...
}
The command npm run start:mocks
starts the mock server and keeps it running.
npm run start:mocks
> node-microservice-architecture@0.0.1 start:mocks
> npx mockserver -p 3001 -m mocks
Mockserver serving mocks {verbose:true} under "mocks" at http://localhost:3001
Sending a notification 🔗
We adjust the controller of the endpoint DELETE /todos/:id
to send a notification immediately after a successful delete.
Note: If the creation of the notification fails, then also
DELETE /todos/:id
returns with an error. But the todo will still be created in the database. I don't recommend to send notifications synchronously with the incoming request. In a later post, we improve on this with asynchronous notification creation.
import { NextFunction, Request, Response } from "express";
import { getToken } from "../../jwt-token";
import { sendNotification } from "./notification.service";
import { deleteTodo } from "./todo.dao";
export async function deleteTodoController(
request: Request,
response: Response,
next: NextFunction
): Promise<void> {
try {
const workspaceId = getToken(response).workspaceId;
await deleteTodo(workspaceId, request.params.id);
await sendNotification(`Deleted todo ${request.params.id}`);
response.sendStatus(204);
} catch (error) {
next(error);
}
}
The unit test of the controller should not try to send a notification. Instead, we mock the function sendNotification
. Since the function is in its own module, mocking this function is identical to mocking any other internal module.
import request from "supertest";
import { v4 as uuid } from "uuid";
import { server } from "../../test.functions";
import { deleteTodoController } from "./delete-todo.controller";
// ...
jest.mock("./notification.service");
// ...
describe("deleteTodoController", () => {
const route = "/todos/:id";
const app = server((app) => {
app.delete(route, deleteTodoController);
});
it("deletes the todo, sends a notification and returns a 204", async () => {
const deleteTodo = require("./todo.dao").deleteTodo;
const sendNotification = require("./notification.service").sendNotification;
const todoId = uuid();
deleteTodo.mockResolvedValue();
await request(app).delete(route.replace(":id", todoId)).expect(204);
expect(deleteTodo).toHaveBeenCalledWith(workspaceId, todoId);
expect(sendNotification).toHaveBeenCalledWith(`Deleted todo ${todoId}`);
});
// ...
});
Conclusion 🔗
We did setup axios
to send HTTP requests and organized the calls into a service module similarly to the organization of data access modules. Locally, the HTTP requests go to a mock server. In tests, the encapsulating module notification.service
is mocked.
In the following posts, we setup connection pooling, logging and authentication of outgoing requests.
Become a GitHub sponsor to access the code of this post as a GitHub repo.