DuyLee

Published

- 10 min read

NestJS Microservice: Xây dựng API Gateway với gRPC - Part 1/2

img of NestJS Microservice: Xây dựng API Gateway với gRPC - Part 1/2

Giới Thiệu về NestJS Microservice

NestJS là một framework hiện đại, mạnh mẽ được xây dựng trên nền tảng Node.js, với mục tiêu giúp các lập trình viên phát triển các ứng dụng phía server một cách hiệu quả và dễ dàng. Một trong những tính năng nổi bật của NestJS là khả năng hỗ trợ mạnh mẽ cho kiến trúc microservice, một phong cách thiết kế phần mềm đang ngày càng phổ biến trong việc xây dựng các hệ thống lớn và phức tạp.

Tại sao API Gateway là cần thiết trong kiến trúc Microservice?

Khi phát triển các ứng dụng lớn với kiến trúc microservice, chúng ta thường gặp phải thách thức trong việc quản lý và phối hợp giữa các dịch vụ nhỏ. Mỗi dịch vụ có thể được viết bằng ngôn ngữ khác nhau, chạy trên các môi trường khác nhau và phục vụ các mục đích khác nhau. Để giảm thiểu sự phức tạp này, API Gateway ra đời như một giải pháp hữu hiệu.

API Gateway đóng vai trò như một cổng duy nhất, tiếp nhận tất cả các yêu cầu từ phía người dùng và điều phối chúng đến các dịch vụ thích hợp. Điều này giúp đơn giản hóa giao diện phía khách hàng và giảm bớt gánh nặng lên từng microservice riêng lẻ.

Tại sao chọn NestJS và gRPC?

NestJS là một framework hiện đại cho việc phát triển ứng dụng Node.js, được xây dựng trên TypeScript. Nó cung cấp một bộ công cụ mạnh mẽ và linh hoạt cho việc xây dựng ứng dụng với kiến trúc module hóa và khả năng mở rộng cao. Đặc biệt, NestJS hỗ trợ dễ dàng tích hợp với nhiều loại công nghệ khác nhau, bao gồm cả gRPC.

gRPC, viết tắt của gRPC Remote Procedure Calls, là một framework RPC (Remote Procedure Call) hiệu suất cao do Google phát triển. gRPC cho phép các dịch vụ giao tiếp với nhau một cách hiệu quả và nhanh chóng thông qua các cuộc gọi hàm từ xa, thay vì sử dụng HTTP/REST thông thường. Điều này đặc biệt hữu ích trong các hệ thống microservice yêu cầu khả năng phản hồi cao và băng thông tối ưu.

Mục tiêu của bài viết này

Trong bài viết này, chúng ta sẽ khám phá cách kết hợp sức mạnh của NestJS và gRPC để xây dựng một API Gateway mạnh mẽ cho hệ thống microservice. Bằng cách sử dụng NestJS làm nền tảng và tích hợp gRPC, chúng ta có thể tận dụng những lợi ích vượt trội từ cả hai công nghệ này.

Bài viết này dài nên mình sẽ chia làm 2 phần:

  1. Giới thiếu về microservice với nestjs và gRPC, setup API Gateway
  2. Tạo các microservice Authentication, ProductOrder

Cùng bắt đầu hành trình xây dựng một hệ thống microservice hiện đại và hiệu quả!

Cài đặt và cấu hình NestJS

Google protocol buffer (Protobuf)

Để sử dụng được kết nối gRPC thì chúng ta bắt buộc phải cài Protobuf. vậy Protobuf là gì?

Google Protocol Buffers (Protobuf) là một công cụ mạnh mẽ để tuần tự hóa dữ liệu (data serialization), được phát triển bởi Google. Nó cho phép bạn xác định cấu trúc dữ liệu một cách rõ ràng và hiệu quả, sau đó sử dụng cấu trúc này để tuần tự hóa dữ liệu thành một định dạng nhỏ gọn, nhanh chóng, và dễ dàng truyền tải giữa các hệ thống.

Protobuf được sử dụng rộng rãi trong các hệ thống phân tán và microservices vì khả năng tối ưu hóa tốt hơn so với các định dạng dữ liệu truyền thống như JSON hay XML. Với Protobuf, bạn có thể mô tả các cấu trúc dữ liệu trong một tập tin .proto và sử dụng các công cụ tạo mã (code generation) để tạo ra các lớp dữ liệu cho nhiều ngôn ngữ lập trình khác nhau. Bạn có thể tìm hiểu thêm tại https://protobuf.dev/overview.

Cách cài đặt

Trên macOS, bạn có thể thực hiện việc này chỉ bằng lệnh brew install protobuf.

Trên Linux hãy thử làm như sau:

   cd ./~
export PROTOBUF_VERSION = 3.11.4 // Đổi version phù hợp
curl - sOL "https://github.com/google/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip" && \
unzip protoc-*.zip && \
mv bin/protoc /usr/local/bin && \
mv include/* /usr/local/include && \
rm -f protoc-*.zip

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 dự án API gateway mới bằng NestJS CLI và tạo 3 file proto trong api-gateway

   mkdir grpc-microservice
nest new api-gateway -p pnpm
cd api-gateway
touch proto/auth.proto && touch proto/product.proto && touch proto/order.proto

Mình sử dụng pnpm làm package manager bạn cũng có thể sử dụng npm để thay thế. Sau khi setup, project của chúng ta sẽ như sau:

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;
  int32 orderId = 2;
  int32 quantity = 3;
}

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

Cài đặt các package

Chúng ta cần cái các package microservice để chuyển tiếp các yêu cầu HTTP tới microservice của mình

   pnpm i @nestjs/microservices @grpc/grpc-js @grpc/proto-loader
pnpm i -D @types/node ts-proto

Setup 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

Thêm 4 script này vào package.json

   "proto:auth": "protoc --plugin=node_modules/.bin/protoc-gen-ts_proto -I=./proto --ts_proto_out=src/auth/ 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=./proto --ts_proto_out=src/order/ 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=./proto --ts_proto_out=src/product/ proto/product.proto --ts_proto_opt=nestJs=true --ts_proto_opt=fileSuffix=.pb",
"proto:all": "pnpm run proto:auth && pnpm run proto:order && pnpm run proto:product",

chạy script:

   pnpm run proto:all
  • proto:all sẽ tạo các tệp protobuf với hậu tố .pb.ts bên trong các module: authorder và product

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: 'proto/auth.proto'
				}
			}
		])
	],
	controllers: [AuthController],
	providers: [AuthService],
	exports: [AuthService]
})
export class AuthModule {}

Auth Service

   import { Inject, Injectable, OnModuleInit } 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 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)
	}

	public async validate(token: string): Promise<ValidateResponse> {
		return await 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 } from '@nestjs/common'
import { ClientGrpc } from '@nestjs/microservices'
import { Observable } from 'rxjs'
import {
	AuthServiceClient,
	RegisterResponse,
	RegisterRequest,
	AUTH_SERVICE_NAME,
	LoginRequest,
	LoginResponse,
	ValidateResponse,
	ValidateRequest
} 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)
	}

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

	@Post('validate')
	private async validate(@Body() body: ValidateRequest): Promise<Observable<ValidateResponse>> {
		return this.svc.validate({ token: body.token })
	}
}

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: 'proto/order.proto'
				}
			}
		])
	],
	controllers: [OrderController]
})
export class OrderModule {}

Order controller

   import { Controller, Inject, Post, OnModuleInit, UseGuards, Req, Body } 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,
		@Body() body: CreateOrderRequest
	): Promise<Observable<CreateOrderResponse>> {
		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: '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 })
	}
}

Kiểm tra và chạy thử

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

   pnpm run start:dev

Trên đây là demo về cách tạo API Gateway microservice đơn giản với nestjs gRPC. phần sau mình sẽ setup các service auth, orderproduct và kết nối chúng thông qua gRPC, cùng đón chờ nha ^^

Cảm ơn mọi người ^^