DEV Community

Cover image for Best Practices for Building Microservices with NestJS
Ezile Mdodana
Ezile Mdodana

Posted on

Best Practices for Building Microservices with NestJS

Microservices architecture has become a popular approach for developing scalable and maintainable applications. NestJS, a progressive Node.js framework, offers robust support for building microservices with its modular architecture and powerful features. In this article, we'll explore best practices for building microservices with NestJS, providing clear details and examples to help you implement these practices effectively.

1. Design Microservices with a Clear Domain

Best Practice
Start by defining the boundaries of each microservice based on the business domains. Each microservice should have a single responsibility and encapsulate a specific business capability.

Example
Suppose we are building an e-commerce platform. We can define the following microservices:

User Service: Handles user authentication, registration, and profile management.
Product Service: Manages product listings, inventory, and pricing.
Order Service: Processes customer orders and handles payment transactions.

By designing microservices around specific business domains, we ensure better scalability and maintainability.

2. Use a Consistent Communication Protocol

Best Practice

Choose a consistent communication protocol for inter-service communication. Common protocols include HTTP, gRPC, and message brokers like RabbitMQ or Kafka. NestJS supports various communication strategies, allowing you to choose the one that best fits your needs.

Example

Using RabbitMQ for event-driven communication between microservices:

// user.service.ts
import { Controller } from '@nestjs/common';
import { EventPattern } from '@nestjs/microservices';

@Controller()
export class UserService {
  @EventPattern('user_created')
  handleUserCreated(data: any) {
    console.log('User created event received:', data);
    // Process the event
  }
}

// product.service.ts
import { Controller } from '@nestjs/common';
import { ClientProxy, ClientProxyFactory, Transport } from '@nestjs/microservices';

@Controller()
export class ProductService {
  private client: ClientProxy;

  constructor() {
    this.client = ClientProxyFactory.create({
      transport: Transport.RMQ,
      options: {
        urls: ['amqp://localhost:5672'],
        queue: 'product_queue',
      },
    });
  }

  createProduct(product: any) {
    // Create product logic
    this.client.emit('product_created', product);
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Implement API Gateways

Best Practice

Use an API gateway to aggregate multiple microservice endpoints into a single entry point. This simplifies client interactions and allows for better management of cross-cutting concerns like authentication, rate limiting, and logging.

Example

Creating an API gateway with NestJS:

import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';

@Module({
  imports: [
    ClientsModule.register([
      { name: 'USER_SERVICE', transport: Transport.RMQ, options: { urls: ['amqp://localhost:5672'], queue: 'user_queue' } },
      { name: 'PRODUCT_SERVICE', transport: Transport.RMQ, options: { urls: ['amqp://localhost:5672'], queue: 'product_queue' } },
    ]),
  ],
})
export class ApiGatewayModule {}
Enter fullscreen mode Exit fullscreen mode

4. Implement Robust Error Handling

Best Practice

Implement consistent and robust error handling mechanisms across all microservices. This includes handling exceptions, retries, and fallback mechanisms.

Example

Using an exception filter in NestJS:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';

@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception.getStatus();

    const errorResponse = {
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: ctx.getRequest().url,
    };

    response.status(status).json(errorResponse);
  }
}

// In your main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpErrorFilter } from './common/filters/http-error.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpErrorFilter());
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

5. Secure Microservices

Best Practice

Implement security best practices to protect your microservices. This includes using TLS/SSL, API keys, OAuth, and JWT for authentication and authorization.

Example

Securing an endpoint with JWT in NestJS:

// auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register({
      secret: 'secretKey',
      signOptions: { expiresIn: '60s' },
    }),
  ],
  providers: [JwtStrategy],
  exports: [PassportModule, JwtModule],
})
export class AuthModule {}

// jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'secretKey',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

// In your controller
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('protected')
export class ProtectedController {
  @Get()
  @UseGuards(AuthGuard('jwt'))
  getProtectedResource() {
    return 'This is a protected resource';
  }
}
Enter fullscreen mode Exit fullscreen mode

6. Implement Health Checks and Monitoring

Best Practice

Implement health checks and monitoring to ensure the availability and performance of your microservices. Use tools like Prometheus, Grafana, or NestJS built-in health checks.

Example

Adding a health check endpoint in NestJS:

import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { TypeOrmModule } from '@nestjs/typeorm';
import { HealthController } from './health.controller';

@Module({
  imports: [TerminusModule, TypeOrmModule.forRoot()],
  controllers: [HealthController],
})
export class AppModule {}

// health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      async () => this.db.pingCheck('database'),
    ]);
  }
}
Enter fullscreen mode Exit fullscreen mode

7. Use CI/CD for Continuous Deployment

Best Practice

Implement continuous integration and continuous deployment (CI/CD) pipelines to automate the testing, building, and deployment of your microservices. Use tools like GitHub Actions, Jenkins, or GitLab CI/CD.

Conclusion

Building microservices with NestJS offers a powerful and flexible approach to developing scalable and maintainable applications. By following these best practices, you can ensure your microservices are well-designed, secure, and resilient. Embrace the modular architecture of NestJS, leverage its rich ecosystem, and implement robust communication and error-handling strategies to create a successful microservices-based system.

My way is not the only way!

Top comments (0)