Telerik blogs

Building a microservice architecture can feel like a lot of work, but NestJS has tools that make the whole process much easier. Learn about the microservice architecture and then build a simple microservice using NestJS.

As software systems grow in complexity, we developers are constantly looking for better ways to build scalable and maintainable applications. One solution that has gained much attention is microservice architecture, which is simply a way to break large applications into smaller, independent services that can work together.

NestJS is a powerful Node.js framework, and it has become a popular choice among developers looking to to create efficient microservices. NestJS has a modular structure and some cool, developer-friendly features, which help simplify building distributed systems.

In this article, we’ll discuss the microservice architecture, its advantages and finally build a simple Math and String microservice using NestJS.

Why Microservices?

Microservices offer powerful advantages compared to the old-school monolithic approach by breaking down complex applications into smaller, independent pieces. Some advantages are:

Scalability

Microservices give us the flexibility to scale specific components independently. For instance, if you have a payment gateway that faces increased traffic, you can scale the system up without affecting the other parts of the system. This allows more efficient use of resources.

Faster Development and Deployment

Each microservice operates independently, allowing development teams work on different parts of the application without the need to wait for others to complete their tasks. This significantly speeds up the development process and makes it much easier to deploy updates continuously.

Fault Isolation

With the old-school monolithic system, any failure from any components would likely crash your entire application. Microservices address this issue by helping you isolate services. If one service or module fails, the rest of the application continues to operate, minimizing the downtime of your applications.

Technology Flexibility

Microservices allow your team the freedom and flexibility to select the most suitable tools and technologies for each service. For instance, one of your services might be built with Node.js, while another could use Python, depending on its specific needs.

Improved Maintenance

Working with smaller, specialized services feels much simpler. It is easier to wrap your head around what each one does, make changes and track down bugs.

Why NestJS for Microservices?

NestJS stands out for microservice development because of its opinionated structure, which minimizes overhead when setting up and managing services. It also supports asynchronous programming, and its compatibility with TypeScript enables developers to write clean and maintainable code. Also, its built-in tools for communication between services make it easier to manage distributed systems.

Designing a Microservice Architecture

Creating a microservice architecture requires thoughtful planning and execution to equip scalability, reliability and long-term maintainability. NestJS simplifies this process thanks to its modular design and user-friendly approach for developers.

Break Down the Application

First, you need to identify the unique functionalities within your application and break them down into separate microservices. If, for instance, you are building an ecommerce platform, you could have services dedicated to user management, product catalogs, order processing and payments.

Each service should focus on a specific task and interact with others through APIs or message brokers. By creating a separate module for each service, you keep it independent and self-contained.

For example:

@Module({
  controllers: [OrderController],
  providers: [OrderService],
})
export class OrderModule {}

Communication Between Microservices

NestJS supports multiple communication methods for microservices, such as:

  • HTTP: Simple and suitable for synchronous communication.
  • Message Brokers: We can use tools like RabbitMQ or Kafka to enable asynchronous communication, so services don’t block each other.
  • Redis or gRPC: These are efficient options for high-performance and real-time interactions.

Database Design

It’s important to avoid using a shared database across services, as this creates tight coupling. Instead, each service should have a dedicated database. Tools like TypeORM or Prisma can be used within NestJS to manage data interactions for each service separately.

Putting It Together

Let’s build a simple Math and String microservice. For the Math service, we will create a microservice that handles arithmetic operations (e.g., addition and multiplication). Then, we create a simple microservice for the String service that handles string operations (e.g., concatenation, capitalization). Finally, we will create another Nest application as a gateway to communicate with our microservices.

We will use TCP transport to keep it simple, but you can replace it with RabbitMQ, Redis or any other protocol for a production-ready setup.

Project Setup

Run the following commands to create a project directory:

mkdir multi-microservice-demo && cd multi-microservice-demo
nest new math-service
nest new string-service
nest new client-app

Navigate to each project and run this command to install the required microservices package:

npm install --save @nestjs/microservices

Build the Math Microservice

Let’s create a controller to handle math operations. Open the math-service folder and add the following to the src/math.controller.ts file:

import { Controller } from "@nestjs/common";
import { MessagePattern } from "@nestjs/microservices";

@Controller()
export class MathController {
  @MessagePattern({ cmd: "sum" })
  calculateSum(data: number[]): number {
    return data.reduce((a, b) => a + b, 0);
  }

  @MessagePattern({ cmd: "multiply" })
  calculateProduct(data: number[]): number {
    return data.reduce((a, b) => a * b, 1);
  }
}

In the MathController class, we define two mathematical operations: summing and multiplying a list of numbers. The @MessagePattern decorator listens for specific message patterns from other microservices.

The calculateSum method listens for the sum command and expects an array of numbers. It calculates the sum of the numbers using the reduce() method, which iterates over the array and adds each value together, returning the total sum.

Similarly, the calculateProduct method listens for the multiply command and multiplies the numbers in the array using reduce. It starts with an initial value of 1 (the identity value for multiplication) and multiplies each element in the array, returning the product.

Both methods are part of the Math service, which allows it to process mathematical tasks based on commands from other microservices.

Next, let’s register the controller in AppModule. Update the src/app.module.ts file with the following:

import { Module } from "@nestjs/common";
import { MathController } from "./math.controller";

@Module({
  controllers: [MathController],
})
export class AppModule {}

We need to modify the entry point to run the microservice. Update the src/main.ts file with the following:

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { Transport, MicroserviceOptions } from "@nestjs/microservices";

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.TCP,
      options: { host: "127.0.0.1", port: 3001 },
    }
  );
  await app.listen();
  console.log("Math Service is running on port 3001");
}
bootstrap();

The main.ts file above sets up a basic NestJS microservice using TCP as the transport protocol. The bootstrap function initializes the microservice, binds it to the local IP address (127.0.0.1), and listens on port 3001. The microservice runs with the logic defined in AppModule.

Once the microservice starts, we also log a message to the console confirming that the service is running and ready to accept incoming requests.

Next, run the Math Microservice:

npm run start

Build the String Microservice

Open the string-service folder and create a controller for string operations. Update the src/string.controller.ts file with the following:

import { Controller } from "@nestjs/common";
import { MessagePattern } from "@nestjs/microservices";

@Controller()
export class StringController {
  @MessagePattern({ cmd: "concat" })
  concatenateStrings(data: string[]): string {
    return data.join(" ");
  }

  @MessagePattern({ cmd: "capitalize" })
  capitalizeString(data: string): string {
    return data.toUpperCase();
  }
}

In the StringController class, we define two message-handling methods using the @MessagePattern() decorator from NestJS. The @MessagePattern() decorator listens for incoming messages with specific command patterns (cmd) from other microservices.

The first method, concatenateStrings, listens for the concat command and expects an array of strings as data. It then joins the strings together with spaces, returning the concatenated result.

The second method, capitalizeString, listens for the capitalize command and transforms the given string to uppercase before returning it. These methods are part of the string manipulation service that enables it to respond to commands from other microservices in the system.

Next, let’s register the controller in AppModule. Update the src/app.module.ts file with the following:

import { Module } from "@nestjs/common";
import { StringController } from "./string.controller";

@Module({
  controllers: [StringController],
})
export class AppModule {}

Next, let’s modify the entry point like by updating the src/main.ts file with the following:

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { Transport, MicroserviceOptions } from "@nestjs/microservices";

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.TCP,
      options: { host: "127.0.0.1", port: 3002 },
    }
  );
  await app.listen();
  console.log("String Service is running on port 3002");
}
bootstrap();

In the main.ts file above, we set up a basic NestJS microservice using TCP as the transport protocol. The bootstrap function initializes the microservice, binds it to the local IP address (127.0.0.1) and listens on port 3002. The microservice runs with the logic defined in AppModule. Once the microservice starts, we log a message to the console confirming that the service is running and ready to accept incoming requests.

Run this command in your terminal to run the String Microservice:

npm run start

Build the Client Application

Now, let’s create a gateway service to communicate with both microservices. Open the client-app folder and update the src/app.service.ts file with the following:

import { Injectable } from "@nestjs/common";
import {
  ClientProxy,
  ClientProxyFactory,
  Transport,
} from "@nestjs/microservices";

@Injectable()
export class AppService {
  private mathClient: ClientProxy;
  private stringClient: ClientProxy;

  // below, we initialize the two client proxies (mathClient and stringClient) to enable communication with them using TCP. Each proxy is configured to connect to a specific service running on localhost but on different ports (3001 for the math service and 3002 for the string service).
  constructor() {
    this.mathClient = ClientProxyFactory.create({
      transport: Transport.TCP,
      options: { host: "127.0.0.1", port: 3001 },
    });
    this.stringClient = ClientProxyFactory.create({
      transport: Transport.TCP,
      options: { host: "127.0.0.1", port: 3002 },
    });
  }

  async calculateSum(numbers: number[]): Promise<number> {
    return this.mathClient.send({ cmd: "sum" }, numbers).toPromise();
  }

  async capitalizeString(data: string): Promise<string> {
    return this.stringClient.send({ cmd: "capitalize" }, data).toPromise();
  }
}

The AppService class above manages communication with the two microservices mathClient and stringClient. The ClientProxyFactory.create() method is used to create the two client proxies mathClient and stringClient, which allow the service to communicate with other microservices over TCP. Each proxy is configured to connect to a specific service running on localhost but on different ports.

The calculateSum and capitalizeString methods send messages to their respective microservices using the .send() method, which passes the command and data. The responses are returned as promises, resolved using .toPromise(). This setup allows the AppService to delegate tasks to other services, for a modular and distributed architecture.

Now, we can update the app controller to use this service. Update the src/app.controller.ts file with the following:

import { Controller, Get } from "@nestjs/common";
import { AppService } from "./app.service";

@Controller() //marks this controller as a Nest.js controller
export class AppController {
  // inject AppService dependency into this conroller
  constructor(private readonly appService: AppService) {}

  // Map HTTP GET requests sent to the "/sum" endpoint to the appService's "calculateSum" method.
  @Get("sum")
  async getSum(): Promise<number> {
    return this.appService.calculateSum([5, 10, 15]);
  }

  // Map HTTP GET requests sent to the "/capitalize" endpoint to the appService's "capitalizeString" method.
  @Get("capitalize")
  async getCapitalizedString(): Promise<string> {
    return this.appService.capitalizeString("hello world");
  }
}

The AppController class here handles HTTP requests and delegates the logic to the AppService class. The @Controller() decorator marks the class as a controller in NestJS, making it capable of handling incoming HTTP requests. The AppService is injected into the controller via dependency injection, allowing it to access methods like calculateSum and capitalizeString.

The @Get('sum') and @Get('capitalize') decorators define HTTP GET endpoints. When a GET request is made to /sum, the getSum method is called, which in turn calls the calculateSum method in the AppService.

Similarly, a GET request to /capitalize triggers the getCapitalizedString method, calling the capitalizeString method from AppService. These endpoints allow interaction between the client and the microservices for performing math and string operations.

We can run the command below to run the Client Application:

npm run start

Test the System

To test what we have so far, we first have to start each microservice.

Run the command below to start the math-service:

cd math-service && npm run start

Next, run this to start the string-service:

cd string-service && npm run start

Finally, start the client app:

cd client-app && npm run start

Test the Endpoints

Best Practices for Microservices

When working with microservices, following some practices can help make your system efficient and easy to maintain in the long run. Let’s go through some key practices to follow when building microservices with NestJS.

Focus on Small, Independent Services

Each service should handle a specific business function—whether it’s user authentication, payments or inventory management. This makes each service easier to maintain and scale independently.

Loose Coupling Is Key

A good microservices architecture keeps services independent, making each service easier to update or replace without affecting the rest of the system. Communication between services should not be direct but should happen through APIs or message brokers like RabbitMQ or Kafka.

Use an API Gateway

Instead of having clients call multiple services directly, introduce an API gateway. It’s not just about routing traffic; it also simplifies tasks like load balancing, authentication and rate limiting. This layer acts as a single entry point for all requests.

Conclusion

Building a microservice architecture can feel like a lot of work, but NestJS has tools that make the whole process much easier. With its built-in support for communication protocols like Redis, Kafka and RabbitMQ, you can connect your services effortlessly and create powerful distributed systems.


About the Author

Christian Nwamba

Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.

Related Posts

Comments

Comments are disabled in preview mode.