+7

NestJS: Microservices với gRPC, API Gateway, và Authentication — Part 1/2

NestJS: Microservices với gRPC, API Gateway, và Authentication — Part 1/2

Hôm nay, tôi muốn giới thiệu với bạn về Microservices trong NestJS (TypeScript) kết hợp với framework gRPC của Google và API Gateway để xử lý các yêu cầu HTTP đến và xác thực dựa trên JWT. Bài viết này rất dài, vì vậy tôi đã chia nó thành 2 phần.

  • Introduction, Preparation, Databases, Shared Proto Project
  • Authentication Microservice, Product Microservice, Order Microservice

Application Infrastructure

Trong bài viết này, tôi quyết định viết mã cho một dự án Microservice thương mại điện tử đơn giản với API Gateway quản lý các yêu cầu HTTP đến và chuyển tiếp chúng đến các Microservices, tổng cộng sẽ có 3 service.

  • Service đầu tiên sẽ là xác thực, nơi người dùng có thể đăng ký và đăng nhập trong khi chúng tôi xác thực quyền của yêu cầu.
  • Service thứ hai sẽ xử lý sản phẩm, tạo sản phẩm mới và tìm sản phẩm dựa trên ID của nó.
  • Service thứ ba sẽ xử lý các đơn hàng cho ứng dụng thương mại điện tử nhỏ của chúng ta.

Bạn sẽ thấy rằng mỗi dịch vụ này đơn giản nhất có thể để giới thiệu bạn về những điều cơ bản của microservices trong NestJS và TypeScript, mà không làm phức tạp ứng dụng của chúng ta.

Trước khi chúng ta bắt đầu với hướng dẫn này, hãy nói ngắn gọn về các framework và khái niệm mà chúng ta sẽ sử dụng ở đây.

NestJS là gì?

NestJS là một framework để xây dựng các ứng dụng web Node.js hiệu quả và có khả năng mở rộng. Nó sử dụng JavaScript hiện đại và được xây dựng bằng TypeScript. Nếu bạn phát triển một API bằng TypeScript, thì NestJS là lựa chọn lý tưởng! Nó được lấy cảm hứng mạnh mẽ từ Spring và Angular.

gRPC là gì?

gRPC là một framework RPC hiện đại, mã nguồn mở, hiệu suất cao có thể chạy trong mọi môi trường. Nó có thể kết nối các dịch vụ trong và giữa các trung tâm dữ liệu một cách hiệu quả với sự hỗ trợ có thể cắm được cho cân bằng tải, theo dõi, kiểm tra sức khỏe và xác thực.

API Gateway là gì?

API Gateway là một điểm đầu vào cho tất cả các khách hàng, trong trường hợp của chúng ta, cho tất cả các yêu cầu từ khách hàng dựa trên HTTP. Yêu cầu chỉ đơn giản được chuyển tiếp/định tuyến đến service phù hợp. Nó xử lý các yêu cầu khác bằng cách phân tán đến nhiều service.

Các yêu cầu tiên quyết

Bạn cần có hiểu biết cơ bản về TypeScript, RPC, Git (+ Github), PostgreSQL mà bạn đã cài đặt trên máy của mình. Tôi sẽ chọn Visual Studio Code làm trình soạn thảo mã nguồn của mình. Bạn có thể sử dụng bất kỳ trình soạn thảo nào mà bạn ưa thích.

Database

Đầu tiên, chúng ta cần tạo 3 cơ sở dữ liệu PostgreSQL. Điều này là do chúng ta sẽ tuân theo mô hình Cơ sở dữ liệu cho mỗi service. Mỗi microservice sẽ có cơ sở dữ liệu riêng của nó để độc lập khi quản lý dữ liệu.

psql postgres
CREATE DATABASE micro_auth;
CREATE DATABASE micro_product;
CREATE DATABASE micro_order;

Tạo project

Tiếp tục với NestJS. Chúng ta sẽ cài đặt NestJS CLI.

npm i -g @nestjs/cli

Chúng ta khởi tạo 4 dự án NestJS mới bằng CLI của nó. Ngoài ra, chúng ta tạo một dự án cho các tệp proto mà chúng ta sẽ chia sẻ qua Github.

mkdir grpc-nest-proto
nest new grpc-nest-api-gateway -p npm
nest new grpc-nest-auth-svc -p npm
nest new grpc-nest-product-svc -p npm
nest new grpc-nest-order-svc -p npm

Shared Proto Repository

Đầu tiên chúng ta cần khởi tạo project grpc-proto

cd grpc-nest-proto 
npm init --y 
git init 
mkdir proto 
touch proto/auth.proto && touch proto/product.proto && touch proto/order.proto 
code .

project của chúng ta sẽ trông như này

Sau đó, chúng ta sẽ thêm một số mã vào các tệp proto của mình.

proto/auth.proto
syntax = "proto3";

package auth;

service AuthService {
  rpc Register (RegisterRequest) returns (RegisterResponse) {}
  rpc Login (LoginRequest) returns (LoginResponse) {}
  rpc Validate (ValidateRequest) returns (ValidateResponse) {}
}

// Register

message RegisterRequest {
  string email = 1;
  string password = 2;
}

message RegisterResponse {
  int32 status = 1;
  repeated string error = 2;
}

// Login

message LoginRequest {
  string email = 1;
  string password = 2;
}

message LoginResponse {
  int32 status = 1;
  repeated string error = 2;
  string token = 3;
}

// Validate

message ValidateRequest {
  string token = 1;
}

message ValidateResponse {
  int32 status = 1;
  repeated string error = 2;
  int32 userId = 3;
}
proto/order.proto
syntax = "proto3";

package order;

service OrderService {
  rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse) {}
}

message CreateOrderRequest {
  int32 productId = 1;
  int32 quantity = 2;
  int32 userId = 3;
}

message CreateOrderResponse {
  int32 status = 1;
  repeated string error = 2;
  int32 id = 3;
}
proto/product.proto
syntax = "proto3";

package product;

service ProductService {
  rpc CreateProduct (CreateProductRequest) returns (CreateProductResponse) {}
  rpc FindOne (FindOneRequest) returns (FindOneResponse) {}
  rpc DecreaseStock (DecreaseStockRequest) returns (DecreaseStockResponse) {}
}

// CreateProduct

message CreateProductRequest {
  string name = 1;
  string sku = 2;
  int32 stock = 3;
  double price = 4;
}

message CreateProductResponse {
  int32 status = 1;
  repeated string error = 2;
  int32 id = 3;
}

// FindOne

message FindOneData {
  int32 id = 1;
  string name = 2;
  string sku = 3;
  int32 stock = 4;
  double price = 5;
}

message FindOneRequest {
  int32 id = 1;
}

message FindOneResponse {
  int32 status = 1;
  repeated string error = 2;
  FindOneData data = 3;
}

// DecreaseStock

message DecreaseStockRequest {
  int32 id = 1;
}

message DecreaseStockResponse {
  int32 status = 1;
  repeated string error = 2;
}

Sau đó bạn cần public project lên github

API GATEWAY

Trong project này, chúng tôi sẽ chuyển tiếp các yêu cầu HTTP tới microservice của mình.

Cài đặt
 npm i @nestjs/microservices @grpc/grpc-js @grpc/proto-loader 
npm i -D @types/node ts-proto
Cấu trúc dự án
nest g mo auth && nest g co auth --no-spec && nest g s auth --no-spec
nest g mo product && nest g co product --no-spec
nest g mo order && nest g co order --no-spec
touch src/auth/auth.guard.ts
Scripts

Thêm 5 script này vào package.json, thay {{link_git_proto}} bằng link git proto bạn đã public bên trên

"proto:install": "npm i {{link_git_proto}}",
"proto:auth": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./node_modules/grpc-nest-proto/proto --ts_proto_out=src/auth/ node_modules/grpc-nest-proto/proto/auth.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb",
"proto:order": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./node_modules/grpc-nest-proto/proto --ts_proto_out=src/order/ node_modules/grpc-nest-proto/proto/order.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb",
"proto:product": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./node_modules/grpc-nest-proto/proto --ts_proto_out=src/product/ node_modules/grpc-nest-proto/proto/product.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb",
"proto:all": "npm run proto:auth && npm run proto:order && npm run proto:product"

chạy script:

npm run proto:install && npm run proto:all
  • proto:install sẽ cài đặt kho lưu trữ proto được chia sẻ dưới dạng gói NPM
  • proto:all sẽ tạo các tệp protobuf với hậu tố bên .pb.ts trong các mô-đun: auth, orderproduct

dự án sẽ trông như thế này:

Auth

Auth module
import { Global, Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AuthController } from './auth.controller';
import { AUTH_SERVICE_NAME, AUTH_PACKAGE_NAME } from './auth.pb';
import { AuthService } from './auth.service';

@Global()
@Module({
  imports: [
    ClientsModule.register([
      {
        name: AUTH_SERVICE_NAME,
        transport: Transport.GRPC,
        options: {
          url: '0.0.0.0:50051',
          package: AUTH_PACKAGE_NAME,
          protoPath: 'node_modules/grpc-nest-proto/proto/auth.proto',
        },
      },
    ]),
  ],
  controllers: [AuthController],
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}
Auth Service
import { Inject, Injectable } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
import { AuthServiceClient, AUTH_SERVICE_NAME, ValidateResponse } from './auth.pb';

@Injectable()
export class AuthService {
  private svc: AuthServiceClient;

  @Inject(AUTH_SERVICE_NAME)
  private readonly client: ClientGrpc;

  public onModuleInit(): void {
    this.svc = this.client.getService<AuthServiceClient>(AUTH_SERVICE_NAME);
  }

  public async validate(token: string): Promise<ValidateResponse> {
    return firstValueFrom(this.svc.validate({ token }));
  }
}
Auth Guard
import { Injectable, CanActivate, ExecutionContext, HttpStatus, UnauthorizedException, Inject } from '@nestjs/common';
import { Request } from 'express';
import { ValidateResponse } from './auth.pb';
import { AuthService } from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {
  @Inject(AuthService)
  public readonly service: AuthService;

  public async canActivate(ctx: ExecutionContext): Promise<boolean> | never {
    const req: Request = ctx.switchToHttp().getRequest();
    const authorization: string = req.headers['authorization'];

    if (!authorization) {
      throw new UnauthorizedException();
    }

    const bearer: string[] = authorization.split(' ');

    if (!bearer || bearer.length < 2) {
      throw new UnauthorizedException();
    }

    const token: string = bearer[1];

    const { status, userId }: ValidateResponse = await this.service.validate(token);

    req.user = userId;

    if (status !== HttpStatus.OK) {
      throw new UnauthorizedException();
    }

    return true;
  }
}
Auth Controller
import { Body, Controller, Inject, OnModuleInit, Post, Put } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { Observable } from 'rxjs';
import { AuthServiceClient, RegisterResponse, RegisterRequest, AUTH_SERVICE_NAME, LoginRequest, LoginResponse } from './auth.pb';

@Controller('auth')
export class AuthController implements OnModuleInit {
  private svc: AuthServiceClient;

  @Inject(AUTH_SERVICE_NAME)
  private readonly client: ClientGrpc;

  public onModuleInit(): void {
    this.svc = this.client.getService<AuthServiceClient>(AUTH_SERVICE_NAME);
  }

  @Post('register')
  private async register(@Body() body: RegisterRequest): Promise<Observable<RegisterResponse>> {
    return this.svc.register(body);
  }

  @Put('login')
  private async login(@Body() body: LoginRequest): Promise<Observable<LoginResponse>> {
    return this.svc.login(body);
  }
}

Order

Order module
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { ORDER_SERVICE_NAME, ORDER_PACKAGE_NAME } from './order.pb';
import { OrderController } from './order.controller';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: ORDER_SERVICE_NAME,
        transport: Transport.GRPC,
        options: {
          url: '0.0.0.0:50052',
          package: ORDER_PACKAGE_NAME,
          protoPath: 'node_modules/grpc-nest-proto/proto/order.proto',
        },
      },
    ]),
  ],
  controllers: [OrderController],
})
export class OrderModule {}
Order controller
import { Controller, Inject, Post, OnModuleInit, UseGuards, Req } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { Observable } from 'rxjs';
import { CreateOrderResponse, OrderServiceClient, ORDER_SERVICE_NAME, CreateOrderRequest } from './order.pb';
import { AuthGuard } from '../auth/auth.guard';
import { Request } from 'express';

@Controller('order')
export class OrderController implements OnModuleInit {
  private svc: OrderServiceClient;

  @Inject(ORDER_SERVICE_NAME)
  private readonly client: ClientGrpc;

  public onModuleInit(): void {
    this.svc = this.client.getService<OrderServiceClient>(ORDER_SERVICE_NAME);
  }

  @Post()
  @UseGuards(AuthGuard)
  private async createOrder(@Req() req: Request): Promise<Observable<CreateOrderResponse>> {
    const body: CreateOrderRequest = req.body;

    body.userId = <number>req.user;

    return this.svc.createOrder(body);
  }
}

Product

Product Module
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { PRODUCT_PACKAGE_NAME, PRODUCT_SERVICE_NAME } from './product.pb';
import { ProductController } from './product.controller';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: PRODUCT_SERVICE_NAME,
        transport: Transport.GRPC,
        options: {
          url: '0.0.0.0:50053',
          package: PRODUCT_PACKAGE_NAME,
          protoPath: 'node_modules/grpc-nest-proto/proto/product.proto',
        },
      },
    ]),
  ],
  controllers: [ProductController],
})
export class ProductModule {}
Product Controller
import { Controller, Get, Inject, OnModuleInit, Param, ParseIntPipe, UseGuards, Post, Body } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { Observable } from 'rxjs';
import {
  FindOneResponse,
  ProductServiceClient,
  PRODUCT_SERVICE_NAME,
  CreateProductResponse,
  CreateProductRequest,
} from './product.pb';
import { AuthGuard } from '../auth/auth.guard';

@Controller('product')
export class ProductController implements OnModuleInit {
  private svc: ProductServiceClient;

  @Inject(PRODUCT_SERVICE_NAME)
  private readonly client: ClientGrpc;

  public onModuleInit(): void {
    this.svc = this.client.getService<ProductServiceClient>(PRODUCT_SERVICE_NAME);
  }

  @Post()
  @UseGuards(AuthGuard)
  private async createProduct(@Body() body: CreateProductRequest): Promise<Observable<CreateProductResponse>> {
    return this.svc.createProduct(body);
  }

  @Get(':id')
  @UseGuards(AuthGuard)
  private async findOne(@Param('id', ParseIntPipe) id: number): Promise<Observable<FindOneResponse>> {
    return this.svc.findOne({ id });
  }
}

API Gateway đã được hoàn thành. Bây giờ chúng ta có thể chạy nó.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí