Microservices with NestJS

Introduction to microservices architecture and how to build microservices using NestJS with gRPC or message queues (e.g., RabbitMQ, Kafka).


API Gateway with NestJS

What is an API Gateway?

An API Gateway acts as a single entry point for all client requests directed to your backend services. Instead of clients interacting directly with multiple microservices, they communicate solely with the API Gateway, which then routes requests to the appropriate microservices, aggregates results, and applies security policies. It's a crucial component in microservice architectures.

Introduction to the API Gateway Pattern

The API Gateway pattern addresses several challenges in distributed systems, particularly microservices. Key benefits and responsibilities include:

  • Request Routing: Directing incoming requests to the correct backend service based on URL path, headers, or other criteria.
  • Request Transformation: Modifying requests before sending them to backend services (e.g., adding headers, transforming data formats).
  • Response Aggregation: Combining responses from multiple backend services into a single response for the client.
  • Authentication & Authorization: Verifying the identity of the client and ensuring they have the necessary permissions to access specific resources.
  • Rate Limiting: Protecting backend services from being overwhelmed by limiting the number of requests from a specific client or IP address.
  • Monitoring & Logging: Collecting metrics and logs about API usage to monitor performance and diagnose issues.
  • Caching: Storing frequently accessed data to reduce latency and load on backend services.

By centralizing these concerns, the API Gateway simplifies client-side development and reduces the complexity of individual microservices. It allows the backend services to focus on their core business logic.

Implementing an API Gateway using NestJS

NestJS is a powerful framework for building efficient, scalable Node.js server-side applications. Its modular architecture, dependency injection, and TypeScript support make it an excellent choice for implementing an API Gateway. Here's a general outline of the process:

  1. Project Setup: Create a new NestJS project using the Nest CLI: nest new api-gateway
  2. Install Dependencies: Install necessary packages for routing, authentication, and making requests to backend services. Common packages include:
    • @nestjs/platform-express (or another platform adapter like Fastify)
    • @nestjs/config (for managing configuration)
    • @nestjs/jwt (for JWT-based authentication)
    • @nestjs/passport (for authentication strategies)
    • axios or node-fetch (for making HTTP requests to backend services)
    • @nestjs/common, @nestjs/core, reflect-metadata, rxjs
  3. Configuration: Define configuration settings for your API Gateway, such as backend service URLs, authentication keys, and rate limiting parameters. Use @nestjs/config to load these settings from environment variables or configuration files.
  4. Create Controllers: Define controllers to handle incoming requests. Each controller method will represent a specific API endpoint.
  5. Create Services: Create services to encapsulate the logic for routing requests, transforming data, and interacting with backend services. Use axios or node-fetch in your services to make HTTP requests.
  6. Implement Authentication & Authorization: Implement authentication and authorization mechanisms using NestJS's built-in support for Passport and JWT. Create guards and decorators to protect specific endpoints.
  7. Implement Request Routing: Use NestJS's routing capabilities to map incoming requests to the appropriate controller methods. Consider using route prefixes to group related endpoints.
  8. Implement Middleware (Optional): Use middleware to intercept requests and responses for logging, request transformation, or other purposes.
  9. Error Handling: Implement global exception filters to handle errors gracefully and provide informative error messages to the client.

Handling Request Routing, Authentication, and Authorization in the API Gateway

Let's break down how to handle routing, authentication, and authorization specifically within the NestJS API Gateway:

Request Routing

NestJS provides a flexible routing system. You define routes using decorators like @Get, @Post, @Put, @Delete, and @Patch on controller methods. You can use route parameters to capture dynamic values in the URL.

Here's an example:

 import { Controller, Get, Param, Query, Req } from '@nestjs/common';
import { ApiGatewayService } from './api-gateway.service';
import { Request } from 'express';

@Controller('products')
export class ProductsController {
  constructor(private readonly apiGatewayService: ApiGatewayService) {}

  @Get()
  async getAllProducts(@Query() query: any, @Req() request: Request): Promise {
    return this.apiGatewayService.routeToProductsService(request.method, request.url, query);
  }

  @Get(':id')
  async getProductById(@Param('id') id: string, @Req() request: Request): Promise {
    return this.apiGatewayService.routeToProductsService(request.method, request.url);
  }

  @Post()
  async createProduct(@Req() request: Request): Promise {
    return this.apiGatewayService.routeToProductsService(request.method, request.url, request.body);
  }
} 

In this example, requests to /products will be handled by the getAllProducts method, and requests to /products/:id will be handled by the getProductById method. The controller then calls a method in the ApiGatewayService to forward the request.

Authentication

NestJS seamlessly integrates with Passport.js for authentication. You can use various authentication strategies like JWT, OAuth2, or local authentication. Here's a general outline for JWT authentication:

  1. Install Dependencies: @nestjs/jwt, @nestjs/passport, passport-jwt
  2. Create a JWT Strategy: Define a Passport strategy that verifies JWT tokens. This strategy will extract the JWT from the request header (usually the Authorization header) and validate its signature.
  3. Create a Guard: Create a Guard that uses the JWT strategy to protect routes. This guard will verify the JWT and, if valid, attach the user information to the request object.
  4. Protect Routes: Use the @UseGuards decorator to apply the guard to specific controllers or methods.

Example code snippets (simplified):

jwt.strategy.ts:

 import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('JWT_SECRET'), // Read from config
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username }; // Customize user payload as needed
  }
} 

jwt.auth.guard.ts:

 import { Injectable, ExecutionContext, CanActivate } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext): boolean | Promise | Observable {
    return super.canActivate(context);
  }
} 

app.module.ts:

 import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { ApiGatewayModule } from './api-gateway/api-gateway.module';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: '.env', // Path to your .env file
      isGlobal: true, // Makes the ConfigService available globally
    }),
    AuthModule,
    UsersModule,
    ApiGatewayModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {} 

auth.module.ts:

 import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtStrategy } from './jwt.strategy';
import { PassportModule } from '@nestjs/passport';

@Module({
  imports: [
    PassportModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'), // Read from config
        signOptions: { expiresIn: '60s' },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {} 

Example Controller:

 import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { Request } from 'express';

@Controller('profile')
export class ProfileController {
  @UseGuards(JwtAuthGuard)
  @Get()
  getProfile(@Req() req: Request) {
    // `req.user` will contain the user information extracted from the JWT.
    return { message: 'Profile data', user: req.user };
  }
} 

Authorization

Authorization determines *what* a user is allowed to do. You can implement authorization using Roles-Based Access Control (RBAC) or Attribute-Based Access Control (ABAC).

  1. Define Roles: Create an enumeration or constant object to define the different roles in your system (e.g., admin, user, guest).
  2. Store User Roles: Store user roles in your database or identity provider.
  3. Create a Roles Guard: Create a custom guard that checks if the user has the necessary role to access a specific resource. This guard will typically access the user's roles from the request object (populated during authentication) and compare them to the required roles.
  4. Apply the Roles Guard: Use the @UseGuards decorator to apply the roles guard to specific controllers or methods.

roles.guard.ts:

 import { Injectable, CanActivate, ExecutionContext, Inject } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!requiredRoles) {
      return true; // No specific roles required, allow access
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user; // Get the user from the request (populated by auth guard)

    if (!user || !user.roles) {
      return false; // User not authenticated or no roles defined
    }

    return requiredRoles.some((role) => user.roles.includes(role)); // Check if user has at least one required role
  }
} 

roles.decorator.ts:

 import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles); 

Example:

 import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { RolesGuard } from './auth/roles.guard'; // Your Roles Guard
import { Roles } from './auth/roles.decorator';   // Your Roles Decorator
import { Request } from 'express';

@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard) // Apply both authentication and authorization
export class AdminController {
  @Get('dashboard')
  @Roles('admin') // Only users with the 'admin' role can access this
  getAdminDashboard(@Req() req: Request) {
    return { message: 'Admin dashboard', user: req.user };
  }
}