DEV Community

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

Posted on • Edited on

Create NestJS Microservices using RabbitMQ - Part 1

In this tutorial, we will build a microservices architecture using NestJS and RabbitMQ. We’ll also demonstrate how to containerize the NestJS microservice application with Docker and Docker Compose.

Technology Stack Overview
We will use the following technology stack:

NestJS: A progressive Node.js framework that adopts a structure similar to Angular. It provides features like dependency injection, TypeScript support, internal microservices, and communication protocol support. For more details, check out the NestJS documentation.

RabbitMQ: A reliable and efficient message broker that uses the AMQP protocol to facilitate communication between microservices.

Postgres with TypeORM: Our database solution, with built-in support in NestJS for TypeORM.

We will follow a specific pattern for the microservices architecture to ensure scalability and maintainability.

main repo
- user (NestJS project)
  - Dockerfile
- token (NestJS project)
  - Dockerfile
.gitsubmodules - contains configs of git submodules used in main repo
docker-compose.yml - contains runtime environment configurations
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

  user-service:
    build:
      context: ./user
      dockerfile: Dockerfile
    ports:
      - "3001:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgres://admin:master123@postgres:5432/postgres
      - RABBITMQ_URL=amqp://rabbitmq
    depends_on:
      - postgres
      - rabbitmq
    networks:
      - backend

  token-service:
    build:
      context: ./token
      dockerfile: Dockerfile
    ports:
      - "3002:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgres://admin:master123@postgres:5432/postgres
      - RABBITMQ_URL=amqp://rabbitmq
    depends_on:
      - postgres
      - rabbitmq
    networks:
      - backend

networks:
  backend:
    driver: bridge

volumes:
  pg_data:
    driver: local
  rabbit_data:
    driver: local
Enter fullscreen mode Exit fullscreen mode

Setting Up the User Service:
Create a fresh NestJS project for the user service using the Nest CLI. In the main.ts file, configure the microservice:

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

Next, in the controller file, develop the routing:

@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

In the app.controller.ts file, add message patterns to communicate between services:

@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 the 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

Connecting User Service to Token Service
Add the following lines to app.module.ts to inform the user service that we are using the token 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 of the user service, connect to the token service:

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

Implement the signup function in app.service.ts of the 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, the token client is a client proxy that is linked to the token service microservice instance. The ‘token_create’ message pattern response from the token service is handled using the Rxjs firstValueFrom method to transform the response from an observable to a promise.

For more examples and event patterns, check out the next part of the blog.

Thanks for reading this. If you have any queries, feel free to email me at harsh.make1998@gmail.com.

Until next time!

Top comments (2)

Collapse
 
saiarkar profile image
Sai Ar Kar

How to handle if the consumer server was down or something went wrong in consumer service?

Collapse
 
hmake98 profile image
Harsh Makwana

To handle downtimes in consumer services like your product service, you can use:
Circuit Breaker: To prevent repeated failures.
Fallback Methods: To provide default responses or cached data.
Retry Mechanisms: To attempt service calls again after a delay.
These strategies help maintain system resilience and user experience.