Nuxtstop

For all things nuxt.js

Create NestJS Microservices using RabbitMQ - Part 1

Create NestJS Microservices using RabbitMQ - Part 1
9 0

In this post, we will be creating a basic microservices architecture using NestJS and rabbitmq.

NestJS is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with and fully supports TypeScript.

There are a few aspects required while building microservices.

  • Is it feasible to use Microservices in current business requirements?
  • Which messaging system do we need to use for internal communication of the services?
  • Choose a database for each service (SQL or NoSQL).
  • Deployment and Maintainance cost.
  • Maintain a large amount of codebase and divide it into requirable modules.

Based on the above points, we're building microservices that will contain user, mailer, token, and post services. contains features like Interfaces, DTOs, Queue & workers.

Let's get started with communication basics. There are two types of microservices applications that you can create in the Nestjs framework. One is with hybrid application creation in which you can expose the microservices with the use of a port. Another one is the Basic application in which you only can connect your service to microservices. we can use this type of application in helper services.

We're going to create an application that will expose APIs of login, signup, forgot password, and change-password with the integration of email sending to multiple users. now for the implementation part, we can first divide these APIs into modules and as per the modules, we can create different services. in this scenario, we also need user authentication. so for the authentication part first we will create a token service, which will be used for generating and decoding the token carried by the request. for the user-related APIs, we can create a user service that will contain APIs related to users only. to send emails to multiple users we can create a mailer service.

The Microservice folder structure will look like this.

application
- user
  - dockerfile
- token
  - dockerfile
- mailer
  - dockerfile
docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

Now first let's configure the docker-compose file as we're using all services together in the development env and docker-compose will be easy to use during development.

 database:
    image: postgres:latest
    container_name: nest-postgres
    ports:
      - 5431:5432
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=master123
      - POSTGRES_DB=postgres
    volumes:
      - database:/var/lib/postgresql/data
    networks:
      - backend
  rabbitmq:
    image: rabbitmq:latest
    container_name: nest-rabbitmq
    hostname: rabbitmq
    volumes:
      - /var/lib/rabbitmq
    ports:
      - "5672:5672"
      - "15672:15672"
    env_file:
      - .env
  cache:
    image: redis:latest
    container_name: nest-redis
    restart: always
    ports:
      - "6379:6379"
    networks:
      - backend
  kong:
    image: kong:latest
    container_name: kong
    restart: on-failure
    command: "kong start"
    volumes:
      - ./kong/kong.yml:/usr/local/kong/declarative/kong.yml
    environment:
      KONG_DATABASE: "off"
      KONG_DECLARATIVE_CONFIG: /usr/local/kong/declarative/kong.yml
      KONG_PROXY_LISTEN: 0.0.0.0:8080
      KONG_PROXY_LISTEN_SSL: 0.0.0.0:8443
      KONG_ADMIN_LISTEN: 0.0.0.0:9000
    ports:
      - "8080:8080"
      - "9000:9000"
    networks:
      - backend
networks:
  backend:
    driver: bridge
volumes:
  database:
    driver: local
  cache:
    driver: local
Enter fullscreen mode Exit fullscreen mode

These are the external services we need in this example. now we can start with our core services.

To combine microservices and APIs Nestjs provides a way to integrate both in the same application. hybrid applications are connected to microservices and they will also expose the APIs on the given port. On the other side, we can also use a core application that is just connected with microservices and doesn't open to any port.

For the user service, we will use a hybrid application and it will be exposed to some port for example 9001.

To interact with services from clients we need some gateway as the middleware. In this example, I'm using the Kong API gateway. it's a lightweight and powerful API gateway to route our request to a specific service. you can find more information about kong gateway from here.

Now, create a fresh Nest project using Nest-CLI for user service. and create microservice in the main.ts file.

To connect with microservices, we're passing rabbitmq URL and queue as an environment variable.

app.connectMicroservice({
    transport: Transport.RMQ,
    options: {
      urls: [`${configService.get('rb_url')}`],
      queue: `${configService.get('user_queue')}`,
      queueOptions: { durable: false },
      prefetchCount: 1,
    },
  });

  await app.startAllMicroservices();
  await app.listen(configService.get('servicePort'));
  logger.log(
    `🚀 User service running on port ${configService.get('servicePort')}`,
  );
Enter fullscreen mode Exit fullscreen mode

and then add APIs to the app.controller.ts file.

  @AllowUnauthorizedRequest()
  @Post('/login')
  login(@Body() data: LoginDto): Promise<IAuthPayload> {
    return this.appService.login(data);
  }

  @AllowUnauthorizedRequest()
  @Post('/signup')
  signup(@Body() data: CreateUserDto): Promise<IAuthPayload> {
    return this.appService.signup(data);
  }

  @Get('/forgot-password')
  getForgotPassword(@CurrentUser() auth: number): Promise<Token> {
    return this.appService.getForgotPasswordToken(auth);
  }

  @Put('/change-password')
  changePassword(
    @Body() data: ForgotPasswordDto,
    @CurrentUser() auth: number,
  ): Promise<void> {
    return this.appService.changePassword(data, auth);
  }
Enter fullscreen mode Exit fullscreen mode

And then in the app.service.ts file add database-related code.

public async signup(data: CreateUserDto) {
    try {
      const { email, password, firstname, lastname } = data;
      const checkUser = await this.userRepository.findUserAccountByEmail(email);
      if (checkUser) {
        throw new HttpException('USER_EXISTS', HttpStatus.CONFLICT);
      }
      const hashPassword = this.createHash(password);
      const newUser = new User();
      newUser.email = data.email;
      newUser.password = hashPassword;
      newUser.firstName = firstname.trim();
      newUser.lastName = lastname.trim();
      newUser.role = Role.USER;
      const user = await this.userRepository.save(newUser);
      const createTokenResponse = await firstValueFrom(
        this.tokenClient.send('token_create', JSON.stringify(user)),
      );
      delete user.password;
      return {
        ...createTokenResponse,
        user,
      };
    } catch (e) {
      throw new InternalServerErrorException(e);
    }
  }
Enter fullscreen mode Exit fullscreen mode

Now, you will find a send function that we're using to send user data to the token service. before we get started on creating a new token service. we need to tell the user service that we're using token service to communicate the data.

First, define the service in app.module.ts to create a token and use it in the app.service.ts file.

import: [
  ClientsModule.registerAsync([
      {
        name: 'TOKEN_SERVICE',
        imports: [ConfigModule],
        useFactory: (configService: ConfigService) => ({
          transport: Transport.RMQ,
          options: {
            urls: [`${configService.get('rb_url')}`],
            queue: `${configService.get('token_queue')}`,
            queueOptions: {
              durable: false,
            },
          },
        }),
        inject: [ConfigService],
      },
]
Enter fullscreen mode Exit fullscreen mode

Now we can use this service token in the app.service.ts file. to use, define it into a constructor.

  constructor(
    @Inject('TOKEN_SERVICE') private readonly tokenClient: ClientProxy,
) {
    this.tokenClient.connect();
}
Enter fullscreen mode Exit fullscreen mode

That's it for the user services configuration. now we can proceed with token service. the data we're sending to the token service will be pushed into a queue and it will get consumed in the token service.

Create another Nest project and then in the main.ts file.

const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.RMQ,
      options: {
        urls: [`${configService.get('rb_url')}`],
        queue: `${configService.get('token_queue')}`,
        queueOptions: { durable: false },
      },
    },
  );
  await app.listen();
  logger.log('🚀 Token service started');
Enter fullscreen mode Exit fullscreen mode

To consume the data from token service. we need to define one message pattern in the app.controller.ts file.

@MessagePattern('token_create')
  public async createToken(@Payload() data: any): Promise<ITokenResponse> {
    return this.appService.createToken(data.id);
  }

  @MessagePattern('token_decode')
  public async decodeToken(
    @Payload() data: string,
  ): Promise<string | JwtPayload | IDecodeResponse> {
    return this.appService.decodeToken(data);
  }
Enter fullscreen mode Exit fullscreen mode

to process function, in the app.service.ts file

public createToken(userId: number): ITokenResponse {
    const accessExp = this.configService.get('accessExp');
    const refreshExp = this.configService.get('refreshExp');
    const secretKey = this.configService.get('secretKey');
    const accessToken = sign({ userId }, secretKey, { expiresIn: accessExp });
    const refreshToken = sign({ userId }, secretKey, { expiresIn: refreshExp });
    return {
      accessToken,
      refreshToken,
    };
  }

  public async decodeToken(
    token: string,
  ): Promise<string | JwtPayload | IDecodeResponse> {
    return decode(token);
  }
Enter fullscreen mode Exit fullscreen mode

so, the token service will process the user data encode or decode it into a token and it will get returned to the user service. and this is how interservice communication will work in nestjs microservices.

In nestjs you can use two types of pattern to send data to other microservices. one is MessagePattern and another one is EventPattern. The difference between both types of the pattern is that in MessagePattern you can get the response from other services but in EventPattern, it will work as an event and is mostly used for background task processing.

You can find a complete working example of this example on my GitHub account. check it here.

Thanks for reading this. If you've any queries, feel free to email me or follow me on Twitter. Until next time!