DEV Community

Cover image for Create NestJS Microservices using RabbitMQ - Part 1
Harsh Makwana
Harsh Makwana

Posted on • Updated on

Create NestJS Microservices using RabbitMQ - Part 1

Using Nestjs and RabbitMQ, we will build a microservices architecture in this tutorial.

We'll also look at how to use Docker and Docker-compose to containerize the Nestjs microservice application.

Let's begin by discussing the technology stack we're using.

We are using the Nestjs framework for the server and it is using a generally angular-like structure. It offers dependency injection, typescript support for third-party libraries, internal microservices, support for communication protocols, and many more features. for more, check out its documentation here.

RabbitMQ will be used as a channel for communication between microservices, since it is more reliable and effective in terms of performance. On the network, it transfers data via the AMQP protocol.

Kong open-source gateway as the API gateway. Kong API gateway offers reverse proxy and using Kong api, we can route our APIs to a particular service. Kong also provides third-party plugins for ex. rate-limiting, JWT authentication.

For database we're using postgres with typeorm as NestJS have built in support for typeorm.

For architecture, we will use the below pattern for the current microservices application.

main repo
- user (NestJS cli project)
  - Dockerfile
- token (NestJS cli project)
  - Dockerfile
.gitsubmodules - (contains configs of git submodules being used in main repo)
docker-compose.yml - will contain runtime env
Enter fullscreen mode Exit fullscreen mode

Let's configure the docker-compose file,

version: "3"
services:
postgres:
  image: postgres:latest
  ports:
    - "5432:5432"
  environment:
    - POSTGRES_USER=admin
    - POSTGRES_PASSWORD=master123
    - POSTGRES_DB=postgres
  volumes:
    - pg_data:/var/lib/postgresql/data
  networks:
    - backend
rabbitmq:
  image: rabbitmq:3-management
  volumes:
    - rabbit_data:/var/lib/rabbitmq
  ports:
    - "5672:5672"
    - "15672:15672"
  networks:
    - backend
networks:
backend:
  driver: bridge
volumes:
pg_data:
  driver: local
Enter fullscreen mode Exit fullscreen mode

Now, create a fresh Nest project using Nest CLI for user service and create a microservice in the main.ts file.
To connect with microservices, we'll use RabbitMQ URL and queue name as a RabbitMQ configuration.

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

Microservice configuration is completed for the NestJS application. now, we will move on controller file to develop routing.

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

Now, create token service in another NestJs project. and 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

Create app.controller.ts file in the token service. to use RabbitMQ as our inter-service communication, we will add message patterns to communicate between the two services. this will create an event listener in the token service.

@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

In app.service.ts add generate token function.

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

Other services that are linked to the RabbitMQ instance use token queue and are ready to provide data to the token service.

In general, having separate queues for each microservice is a good idea to improve communication.

Add the lines below to app.module.ts to inform the user service that we are using the token service in order to connect the token service with the user service.

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

In app.service.ts file of user service. to connect the token service.

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

Create app.service.ts file in user service.

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

Using RabbitMQ, which is configured in the main.ts file of the token service, the token client is a client proxy that is linked to the token service microservice instance in this instance.

We may contact the token service from the user service since the 'token create' message pattern has already been specified in the token service.

An observable in NestJS will be the message pattern response.
In order to transform the answer from the observable to a promise, use the Rxjs firstValueFrom method.

We will see event pattern example in next part of the blog.

Thanks for reading this. If you've any queries, feel free to email me at harsh.make1998@gmail.com.
Until next time!

Top comments (0)