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
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
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')}`);
Next, in the controller file, develop the routing:
@Post('/signup')
signup(@Body() data: CreateUserDto): Promise<IAuthPayload> {
return this.appService.signup(data);
}
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');
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);
}
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);
}
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],
},
]),
]
In app.service.ts of the user service, connect to the token service:
constructor(
@Inject('TOKEN_SERVICE') private readonly tokenClient: ClientProxy,
) {
this.tokenClient.connect();
}
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);
}
}
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)
How to handle if the consumer server was down or something went wrong in consumer service?
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.