📢 ATENCIÓN, este post forma parte de esa serie en la que te pedimos que por favor te pongas comodo porque aqui se vienen detalles muy interesantes y dificiles de resumir, es por ello que aunque hemos intentado dividirlo en dos post estamos conscientes que es un poco más largo de lo que solemos manejar.
Guía paso a paso: Aplicaciones NestJS con TypeScript, gRPC, API Gateway, autenticación y validación
Hoy quiero presentarte los microservicios en NestJS (TypeScript) combinados con el framework gRPC de Google, y API Gateway para manejar las peticiones HTTP entrantes y la autenticación basada en JWT.
🔔🔔 Tenemos Curso de NestJS
Infraestructura de la aplicación
Para este artículo, se codificó un simple proyecto de Microservicio de comercio electrónico con un API Gateway que gestiona las peticiones HTTP entrantes y las reenvía a los Microservicios que serán 3 en total.
- El primer servicio será la autenticación, donde los usuarios pueden registrarse e iniciar sesión mientras validamos la autorización de la solicitud.
- El segundo servicio manejará los productos para crear un nuevo producto pero también para encontrar un producto basado en su ID.
- Esto es importante para el tercer servicio que maneja los pedidos entrantes para nuestra pequeña aplicación de comercio electrónico.
Verás, que cada uno de estos servicios es tan mínimo como sea posible para introducirte en los fundamentos de los microservicios en NestJS y TypeScript, mientras que no se vuela nuestra aplicación.
No nos ocuparemos de las variables de entorno, los plazos, el manejo profundo de errores, docker, y las configuraciones complejas. Así que no te preocupes, ¡lo mantenemos simple!
Cada servicio será un proyecto independiente, así que tenlo en cuenta. Además, tendremos un repositorio compartido, donde almacenaremos y gestionaremos los archivos Proto, lo cual es una práctica común cuando se trabaja con gRPC. Por último, pero no menos importante, crearemos otro proyecto que será el API Gateway. Así que hablamos de 5 proyectos en total.
Antes de empezar con esta guía, vamos a hablar brevemente de los frameworks y conceptos que vamos a utilizar aquí. Puede sonar aburrido, pero sigue conmigo, es esencial saber lo que va a pasar para seguir y entender la aplicación que vamos a construir.
¿Qué es NestJS?
NestJS es un marco de trabajo para construir aplicaciones web Node.js eficientes y escalables. Utiliza JavaScript moderno y está construido con TypeScript. Si desarrollas una API construida con TypeScript, entonces NestJS es el camino a seguir. Está fuertemente inspirado en Spring y Angular.
¿Qué es gRPC?
gRPC es un marco RPC moderno, de código abierto y de alto rendimiento que puede ejecutarse en cualquier entorno. Puede conectar de forma eficiente servicios en y entre centros de datos con soporte conectable para el equilibrio de carga, el rastreo, la comprobación del estado y la autenticación.
¿Qué es una pasarela API (API Gateway)?
Una pasarela API es un punto de entrada para todos los clientes, en nuestro caso, para todas las solicitudes de clientes basadas en HTTP, pero no tiene por qué limitarse sólo a HTTP.
La pasarela de la API gestiona las solicitudes de dos maneras. Algunas solicitudes son simplemente proxies/enrutadas al servicio apropiado. Otras solicitudes se gestionan mediante la distribución a múltiples servicios.
Guía paso a paso
Como se mencionó, este artículo estará dividido en 2 partes. La cobertura será así:
- Bases de datos, Proyecto Proto compartido, API Gateway
- Microservicio de autenticación, Microservicio de productos y Microservicio de pedidos.
Requisitos previos
Se requiere tener un conocimiento básico de TypeScript, RPC, Git (+ Github), PostgreSQL que haya instalado localmente en su máquina.
En este artículo el usado será Visual Studio Code como editor de código. Puedes usar el que prefieras.
Atención
Vas a ver el comando code muy a menudo en este artículo. Este es un comando de Visual Studio Code que abre Visual Studio Code basado en el directorio actual. Si no utiliza VS Code, asegúrate de abrir el directorio correcto en su IDE o editor de código.
Base de datos
Primero, necesitamos crear 3 bases de datos PostgreSQL. Esto es porque seguiremos el patrón de Base de Datos por Servicio. Cada uno de los microservicios tendrá su base de datos para ser incluso independientes a la hora de gestionar los datos.
Todo el mundo maneja eso de manera diferente, algunas personas utilizan algún tipo de GUI, pero vamos a utilizar nuestro terminal. De nuevo, necesitas tener PostgreSQL instalado en tu máquina.
Si tienes PostgreSQL instalado, los siguientes cuatro comandos se ejecutarán en máquinas Linux, Mac y Windows.
$ psql postgres
$ CREATE DATABASE micro_auth;
$ CREATE DATABASE micro_product;
$ CREATE DATABASE micro_order;
$ \l
$ \q
Explicación del comando:
psql postgres
abre el CLI psql con el usuario Postgres.CREATE DATABASE micro_auth;
creación de la base de datos.CREATE DATABASE micro_product;
creación de la base de datos.CREATE DATABASE micro_order;
creación de la base de datos.\l
lista de todas las bases de datos.\q
salir de la CLI de psql
Mi terminal se vería así después de ejecutar los cuatro comandos con éxito. Como podemos ver, las 3 bases de datos fueron creadas con éxito.
Creación de proyectos
Vamos a continuar con NestJS. Vamos a instalar la CLI de NestJS de forma global.
$ npm i -g @nestjs/cli
Inicializamos 4 nuevos proyectos NestJS con su CLI. Además, creamos un proyecto para nuestros archivos proto que vamos a compartir por Github.
Es recomendable crear una carpeta como espacio de trabajo donde vas a ejecutar los siguientes comandos en tu terminal, para tener todos los proyectos en un solo lugar, pero esto depende de ti. Así que no es parte de esta guía.
$ 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
Repositorio de Proto compartido
Así que, como he dicho, vamos a empezar con el proyecto proto compartido. Tenemos que hacer esto porque tenemos que utilizar estos archivos en todos los demás proyectos.
En primer lugar, tenemos que abrir el proyecto en nuestro editor de código, este comando de código. Abre el grpc-proto
proyecto en nuestro editor VSCode.
$ cd grpc-nest-proto
$ npm init --y
$ git init
$ mkdir proto
$ touch proto/auth.proto && touch proto/product.proto && touch proto/order.proto
$ code .
El proyecto tendrá este aspecto:
A continuación, vamos a añadir algo de código a nuestros archivos proto. Sígueme.
Auth Proto
Primero, vamos a crear el archivo Proto para nuestro Servicio de Autenticación. Vamos a añadir 3 endpoints RPC: Register
, Login
, y Validate
Añadamos algo de código a 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;
}
Prototipo de pedido
En segundo lugar, vamos a crear el archivo Proto para nuestro Servicio de Pedidos. Vamos a añadir sólo 1 endpoint RPC llamado CreateOrder
.
Vamos agregar al código 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;
}
Producto Proto
Por último, pero no menos importante, vamos a crear el archivo Proto para nuestro Servicio de Producto. Vamos a añadir 3 puntos finales RPC: CreateProduct
, FineOne
y DecreaseStock
Añadimos a 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;
}
Creación de un repositorio en Github
Como te hemos comentado, tenemos que compartir este proyecto de alguna manera. Para esta guía, fue elegido Github. Así que vamos a crear un repositorio público aquí. Por cierto, este es el único repositorio que necesitas crear en este tutorial. Asi que vamos a instalar este repositorio como un paquete NPM más tarde en nuestros proyectos.
Volvamos a nuestro código porque necesitamos confirmar y hacer el respectivo push de nuestro proyecto.
Sustituye YOUR_USERNAME por tu nombre de usuario en Github.
$ git remote add origin https://github.com/YOUR_USERNAME/grpc-nest-proto.git
$ git add .
$ git commit -m "chore(): init nestjs"
$ git branch -M main
$ git push -u origin main
Eso es todo. Ahora nuestro código debería estar en Github. No vamos a hacer más cambios aquí, vamos a seguir adelante con el API Gateway.
API GATEWAY
En este proyecto, vamos a reenviar nuestras peticiones HTTP a nuestros microservicios. A veces, es necesario autorizar las solicitudes entrantes, y eso es lo que en parte está sucediendo aquí en la combinación con nuestro servicio de autenticación que vamos a codificar más tarde.
En primer lugar, tenemos que abrir el proyecto en nuestro editor de código, así que vuelve a entrar en tu terminal al directorio, donde almacenamos nuestros proyectos.
$ cd grpc-nest-api-gateway
$ code .
Instalación de dependencias
Vamos a instalar algunas dependencias que vamos a necesitar.
$ npm i @nestjs/microservices @grpc/grpc-js @grpc/proto-loader
$ npm i -D @types/node ts-proto
Estructura del proyecto
Finalmente, se continuará con la creación de la estructura final de carpetas y archivos. Para simplificar, vamos a un solo módulo aquí.
$ 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
Añadir scripts
Tenemos que añadir algunos scripts a nuestro package.json
para generar nuestros archivos protobuf basados en el proyecto proto compartido que acabamos de completar.
Simplemente agreguemos estas 5 líneas de código dentro del scripts
propiedad de nuestro archivo package.json
"proto:install": "npm i git+https://github.com/YOUR_USERNAME/grpc-nest-proto.git",
"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"
Así es como se verá en tu package.json
o revisa este archivo en el siguiente repo.
¡Vamos a ejecutar estas secuencias de comandos!
$ npm run proto:install && npm run proto:all
proto:install
instalará nuestro repositorio de prototipos compartido como un paquete NPM.proto:all
generará nuestros archivos protobuf con el sufijo.pb.ts
dentro de nuestros módulos:auth
,order
, yproduct
Así que después de estos pasos, el proyecto debería tener este aspecto:
Ahora, empecemos a codificar.
AuthService
Lo que vamos a hacer aquí es conseguir que nuestro Servicio de Autenticación llame a su método de validación que vamos a codificar más adelante. Pero, ya podemos prepararlo, ya que tenemos su archivo protobuf.
Conocemos su respuesta y su payload.
Cambiemos src/auth/auth.service.ts
desde
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {}
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
Nuestro AuthGuard depende del servicio que acabamos de crear. Aquí obtenemos el token de portador que vamos a obtener más tarde. Luego validamos este token, si es inválido, lanzamos una excepción de no autorizado, por lo que bloqueamos una solicitud para los usuarios sin una autorización válida.
Cambiemos src/auth/auth.guard.ts
desde
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;
}
}
Controlador de autenticación
El Microservicio de Autenticación también se encargará de los nuevos usuarios y de los inicios de sesión de los usuarios, por lo que también añadimos un controlador en nuestra Pasarela de la API para reenviar estas peticiones a nuestro Microservicio de Autenticación.
Cambiemos src/auth/auth.controller.ts
desde
import { Controller } from '@nestjs/common';
@Controller('auth')
export class AuthController {}
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);
}
}
Módulo Auth
Ahora necesitamos registrar nuestro Servicio de Autenticación. Vamos a necesitar hacer este módulo global y exportar nuestro AuthService porque necesitamos usar nuestro AuthGuard, que depende del AuthService, dentro de nuestros otros módulos. Así que ten en cuenta esto.
Cambiemos src/auth/auth.module.ts
desde
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
@Module({
controllers: [AuthController],
providers: [AuthService]
})
export class AuthModule {}
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 {}
Ahora ya hemos terminado con el módulo de autentificación de nuestra API Gateway. Continuemos con el módulo de pedidos. Es mucho más simple que nuestro módulo auth.
Módulo de pedidos
De forma similar a nuestro AuthModule, necesitamos registrar nuestro Microservicio de Pedidos para comunicarnos con él más adelante.
Probablemente, te darás cuenta, que vamos a utilizar un puerto diferente en comparación con el Servicio de Autenticación, esto es necesario porque no podemos utilizar el mismo puerto dos veces.
Vamos a cambiar src/order/order.module.ts
desde
import { Module } from '@nestjs/common';
import { OrderController } from './order.controller';
@Module({
controllers: [OrderController]
})
export class OrderModule {}
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 {}
Controlador de pedidos
Aquí necesitamos codificar solo 1 endpoint porque nuestro Microservicio de Pedidos solo posee 1 endpoint.
Vamos a utilizar nuestro servicio AuthGuard aquí, para validar la autorización de esta solicitud POST que vamos a codificar ahora.
Si el usuario que hace esta solicitud está autorizado, el AuthGuard añade el ID del usuario a la solicitud, que vamos a fusionar con el cuerpo de la solicitud aquí, ya que nuestro archivo protobuf order.pb.ts
requires a User ID.
Vamos con el código src/order/order.controller.ts
import { Controller } from '@nestjs/common';
@Controller('order')
export class OrderController {}
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);
}
}
Módulo de Producto
Ahora vamos a repetir un poco este proceso para nuestro módulo de Producto de nuestro API Gateway.
Seguimos codeando src/product/product.module.ts
import { Module } from '@nestjs/common';
import { ProductController } from './product.controller';
@Module({
controllers: [ProductController]
})
export class ProductModule {}
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 {}
Controlador de Producto
El Microservicio de Producto contiene 3 endpoints, pero DecreaseStock no debería ser alcanzable desde nuestro API Gateway, por eso sólo vamos a añadir los otros 2 endpoints.
Vamos a darle src/product/product.controller.ts
import { Controller } from '@nestjs/common';
@Controller('product')
export class ProductController {}
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 });
}
}
¡Genial! Eso es todo. La puerta de enlace de la API se ha completado. Ahora podemos ejecutarlo. Desafortunadamente, este API Gateway es inútil sin sus microservicios, así que está historia continuará amigos.
$ npm run start:dev
Muchas gracias por llegar hasta el final de este artículo.