Cada vez que empiezo un nuevo proyecto me pregunto: "¿qué tipo de marco debo elegir?".
Por supuesto, depende de múltiples factores, incluida la experiencia del equipo, qué problema se debe resolver, qué escala se espera , etc.
Ok, lo más probable es que sea una arquitectura basada en microservicios. Tal enfoque podría basarse en casi todos los casos a partir de los puntos anteriores. Pero espere, ¿en qué se diferencian los microservicios del monolito desde el punto de vista de la programación? Después de pensar un poco, puedo decir que son casi idénticos pero con una sola excepción: los microservicios necesitan comunicarse entre sí de alguna manera.
Aparte de eso, todas las mejores prácticas y las características requeridas del marco son las mismas:
Hay muchos marcos basados en NodeJS, pero me gustaría compartir mi experiencia en la creación de microservicios con NestJS.
Todos los puntos enumerados anteriormente están cubiertos por el marco NestJS:
Juguemos un poco y creemos dos microservicios con interacción entre sí y un BFF que traducirá los protocolos de comunicación entre servicios en protocolos compatibles con la web.
Como ejemplo sencillo propondría los siguientes casos:
Este es un ejemplo muy simple que representa la separación de responsabilidades y la segregación de datos. Aquí tenemos un concepto de account
que es una combinación de un user
y un profile
. Imaginemos que tenemos un servicio de users
genéricos que se encarga únicamente de la autenticación. Por razones de seguridad, no debemos almacenar credenciales y datos personales en la misma base de datos ni administrarlos en el mismo servicio. Por eso disponemos de un servicio de profiles
dedicado. Es solo una historia, pero nuestro cliente imaginario de alguna manera debería obtener una account
que contenga datos tanto del user
como del profile
. Es por eso que necesitamos un servicio BFF (backend para frontend) que combine datos de diferentes microservicios y regrese al cliente.
Primero instalemos nest-cli
y generemos proyectos nest:
$ mkdir nestjs-microservices && cd nestjs-microservices $ npm install -g @nestjs/cli $ nest new users $ nest new profiles $ nest new bff
De forma predeterminada, NestJS genera un servidor Http, así que actualicemos los users
y los profiles
para que se comuniquen a través de un protocolo basado en eventos, por ejemplo: Redis Pub/Sub:
$ npm i --save @nestjs/microservices
/src/main.ts
const app = await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.REDIS, options: { url: 'redis://localhost:6379', }, }, ); app.listen(() => console.log('Services started'));
Un servicio de NestJS consta de módulos y los módulos se dividen en diferentes partes, pero las más importantes son los controladores y los servicios. Los controladores son responsables de proporcionar una API de servicio externo. Vamos a definirlos para servicios de users
y profiles
.
/users/src/app.controller.ts
@Controller() export class AppController { @MessagePattern({ cmd: 'get_users' }) getUsers() { return this.appService.getUsers(); } }
/users/src/app.service.ts
@Injectable() export class AppService { users = [ { id: '1', login: 'bob' }, { id: '2', login: 'john' }, ]; getUsers(): User[] { return this.users; } }
/profiles/src/app.controller.ts
@Controller() export class AppController { @MessagePattern({ cmd: 'get_profiles' }) getProfiles() { return this.appService.getProfiles(); } }
/profiles/src/app.service.ts
@Injectable() export class AppService { profiles = [ { id: '1', name: 'Bob' }, { id: '2', name: 'John' }, ]; getProfiles(): Profile[] { return this.profiles; } }
En aras de la simplicidad, no he usado bases de datos y solo he definido colecciones en memoria.
Una vez que los servicios están listos, tenemos que crear el servicio BFF que expone la interfaz REST compatible con la web:
/bff/src/app.controller.ts
@Controller() export class AppController { constructor( @Inject('PUBSUB') private readonly client: ClientProxy, ) {} @Get('accounts') async getAccounts(): Promise<Account[]> { const users = await this.client .send<User[]>({ cmd: 'get_users' }, { page: 1, items: 10 }) .toPromise(); const profiles = await this.client .send<Profile[]>({ cmd: 'get_profiles' }, { ids: users.map((u) => u.id) }) .toPromise(); return users.map<Account>((u) => ({ ...u, ...profiles.find((p) => p.id === u.id), })); } }
Como puede ver, hay una dependencia inyectada con el token 'PUBSUB'. Definámoslo también:
/bff/src/app.module.ts
@Module({ imports: [ ClientsModule.register([ { name: 'PUBSUB', transport: Transport.REDIS, options: { url: 'redis://localhost:6379', }, }, ]), ], controllers: [AppController], providers: [AppService, Pubsub], }) export class AppModule {}
En este punto, se implementa el caso #1. ¡Vamos a ver!
Ejecute el siguiente comando contra cada proyecto:
npm run start:dev
Una vez que se inicien los tres servicios, enviemos una solicitud para obtener cuentas:
curl http://localhost:3000/accounts | jq [ { "id": 1, "login": "bob", "name": "Bob" }, { "id": 2, "login": "john", "name": "John" } ]
He usado las siguientes dos herramientas: curl
y jq
. Siempre puede usar sus herramientas preferidas o simplemente seguir el artículo e instalarlas usando cualquier administrador de paquetes con el que se sienta cómodo.
Llamaría su atención que hay dos estilos de mensajes diferentes: asíncrono y sincrónico. Como puede ver en los casos de ejemplo, las imágenes n.° 1 y n.° 2 se definen utilizando el estilo de mensaje de solicitud-respuesta, por lo tanto, síncrono. Se elige porque tenemos que devolver datos solicitados por el cliente. Pero en los casos #3 y #4, puede ver un solo comando de dirección para guardar datos. Esta vez es un estilo de mensaje asincrónico basado en eventos. NestJS proporciona los siguientes decoradores para cubrir ambos casos:
@MessagePattern()
- para el estilo de mensajes sincrónicos@EventPattern()
- para el estilo de mensajes asincrónicos
Los casos n.° 2 y n.° 4 son bastante similares a los casos n.° 1 y n.° 3, por lo que omitiré su implementación en este ejemplo.
Implementemos el caso #3. Esta vez usaremos el método @EventPattern()
emit()
el decorador @EventPattern().
/bff/src/app.controller.ts
@Post('accounts') async createAccount(@Body() account: Account): Promise<void> { await this.client.emit({ cmd: 'create_account' }, account); }
/profiles/src/app.controller.ts
@EventPattern({ cmd: 'create_account' }) createProfile(profile: Profile): Profile { return this.appService.createProfile(profile); }
/profiles/src/app.service.ts
createProfile(profile: Profile): Profile { this.profiles.push(profile); return profile; }
/users/src/app.controller.ts
@EventPattern({ cmd: 'create_account' }) createUser(account: Account): User { const {id, login} = account; return this.appService.createUser({id, login}); }
/users/src/app.service.ts
createProfile(user: User): User { this.users.push(user); return user; }
agreguemos una cuenta mira el resultado:
curl -X POST http:/localhost:3000/accounts \ -H "accept: application/json" -H "Content-Type: application/json" \ --data "{'id': '3', 'login': 'jack', 'name': 'Jack'}" -i HTTP/1.1 201 Created
curl http://localhost:3000/accounts | jq [ { "id": "1", "login": "bob", "name": "Bob" }, { "id": "2", "login": "john", "name": "John" }, { "id": "3", "login": "jack", "name": "Jack" } ]
Consulte el código completo de los servicios descritos anteriormente: https://github.com/artiom-matusenco/amazing-microservices-with-nestjs .
¡Eso es todo! 🙃
Pero espere... ¿qué pasa con el monitoreo, la autorización, el rastreo, la tolerancia a fallas, las métricas...?
Diría que es una buena pregunta y tengo una buena respuesta: en el mejor de los casos, todas estas cosas geniales deben descargarse a nivel de infraestructura; los microservicios solo deberían preocuparse por la lógica empresarial y, probablemente, por algunas cosas específicas, como el seguimiento analítico.
NestJS ofrece la posibilidad de crear microservicios ligeros, bien estructurados y sorprendentes. Las herramientas y características listas para usar hacen que el desarrollo, la extensión y el mantenimiento sean agradables y eficientes.