Best Practices for Building Microservices with NestJS
Tags: nestjs, typescript, microservices, backend
Published: 2024-06-06 06:57:49

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](https://grpc.io/), and message brokers like [RabbitMQ](https://www.rabbitmq.com/) or [Kafka](https://kafka.apache.org/). 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);
  }
}
```


**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 {}
```

**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();
```

**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';
  }
}
```

**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](https://prometheus.io/), [Grafana](https://grafana.com/), 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'),
    ]);
  }
}
```


**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!**