NestJs Framework

Arquitectura de aplicación basada en NestJs Framwork

Antes de comenzar

Para poder entender mejor la arquitectura debería leer primero la documentación disponible del framework en https://docs.nestjs.com/

Consideraciones Generales

  • Todos los nombres de ficheros y carpetas se tratará de usar nombres en singular.
  • Se evitará tener más tres niveles dentro de la carpeta “src”.
  • El modelo se implementará usando TypeOrm.
  • Nunca se crea una entidad con “new” se debe usar el método “create” del modelo.
  • Usar “async” y “await” pero estar pendiente de no bloquear el código innecesariamente.

Módulos

  • La aplicación tendrá un módulo base con el nombre “ApplicationModule” y estará en el fichero “src/app.module.ts”
  • Se tendrá tantos módulos secundarios como sea necesario y el objetivo es que tenga funcionalidades muy relacionadas.

Arquitectura de un módulo

  • La clase módulo tendrá el siguiente nombre <NombreModulo>Module.
  • Estará en el fichero “src/<nombre-module>/<nombre-module>.module.ts”

Arquitectura de un módulo

Nombres de carpetas internas.

  • api: Tendrá las controladoras de la Api Rest de ser necesario implementarlas. Nombre ficheros <nombre-clase>.api.ts
  • entity: Tendrá las clases entidad basadas en TypeOrm. Nombre fichero <nombre-entidad>.ts
  • graphql: Tendrá los ficheros de configuración de los graphql uno por cada entidad. Nombre fichero <nombre-entidad>.graphql
  • model: Tendrá las clases relacionadas con el modelo,  generalmente una por entidad. Nombre de fichero <nombre-entidad>.model.ts
  • resolver: Tendrá las clases controladoras de graphql, generalmente una por entidad. Nombre de fichero <nombre-entidad>.resolver.ts

Entity

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { IsString, IsEmpty } from 'class-validator';

@Entity() //Indicar que es una entidad
export class Organization {

  @PrimaryGeneratedColumn() //Llave primaria autogenerada
  @IsEmpty() //Validación por el api rest no se puede pasar este valor
  id: number;

  @Column({ length: 500 }) //String
  @IsString() //Validación por api rest debe ser un string
  name: string;

  @Column('text')
  @IsString()
  description: string;
}

Model

  • La clase model es la encargada de implementar la lógica de negocio, brindando servicios a las interfaces Api Rest o Graphql.
  • Para acceder a la base de datos utilizan el repositorio de la entidad.
  • Desde el punto de vista de un usuario de la clase este tipo de clases se comporta como un repositorio de TypeOrm.

Model

import { Component } from '@nestjs/common';
import { Organization } from '../entity/organization';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {ModelInterface} from '../../base/proxy-model';

@Component() //Indicar a nestjs que es un servicio
export class OrganizationModel implements ModelInterface { //La interfaz ModelInterce se utiliza en ProxyModel

  constructor(
    @InjectRepository(Organization)  private repository: Repository<Organization> //Recibe el repositorio de TypeOrm
  ) {
  }

  getRepository() { //Método de ModelInterface
    return this.repository;
  }
}

Inyectar Model en DI

@Module({
  imports: [TypeOrmModule.forFeature([Organization])], //Informar a TypeOrm de la entidad
  components: [
    ProxyModel.injectorFactory('Organization'), //Implementar un proxy al model para  que se comporte como un repositorio de TypeOrm
    OrganizationModel, //Injectar el Model
  ],
})
export class OrganizationModule {
}

Uso de Model

@Controller('organization')
export class OrganizationApi {
  constructor(
    @Inject('Organization') private model: OrganizationModel
  ){}

  @Get()
  async list(): Promise<Organization[]> {
    return await this.model.find(); //Método no implementado en el model pero si en el repositorio.
  }
}

Model como Repositorios

Se logra usando la clase “ProxyModel” que básicamente lo que hace es devolver un objeto “Proxy” de javascript que hace lo siguiente.

  • Cuando se llama un método sobre el “Model” si está implementado en el modelo lo usa sino llama al método en el repositorio si existe.
  • Si el método no está implementado en ninguna de las dos clases se comporta como un método indefinido.

Controller Api Rest

@Controller('organization')
export class OrganizationApi {

  constructor(
    @Inject('Organization') private model: OrganizationModel //En el constructor recibe las dependencias.
  ){}
}

Controller Api Rest

@Controller('organization')
export class OrganizationApi {

  @Get() //Indica que es método http get
  async list(): Promise<Organization[]> {
    return await this.model.find(); //Devuelve todas las entidades
  }
}

Controller Api Rest

@Controller('organization')
export class OrganizationApi {

  @Get(':id') //Obetener dado un id
  async findOne(@Param('id', new ParseIntPipe()) id) {//Validar que id es un entero
    return await this.model.findOneById(id); //Obtener el objeto en el modelo
  }
}

Controller Api Rest

@Controller('organization')
export class OrganizationApi {

  @Post()
  async create(@Body(new ValidationPipe()) organization: Organization) { //Validacion de la entidad
    return await this.model.save(organization);
  }
}

Controller Api Rest

@Controller('organization')
export class OrganizationApi {

  @Put(':id')
  async update(
    @Param('id', new ParseIntPipe()) id,
    @Body(new ValidationPipe()) organization: Organization
  ) {
    organization.id = id;
    return await this.model.save(organization);
  }
}

Controller Api Rest

@Controller('organization')
export class OrganizationApi {

  @Delete(':id')
  async remove(@Param('id', new ParseIntPipe()) id) {
    return await this.model.deleteById(id);
  }
}

GraphQL

type Organization {
  id: Int!
  name: String
  description: String
}

type Query {
  organization(id: Int!): Organization
  allOrganization: [Organization]
}

type Mutation {
  createOrganization(name: String, description: String): Organization
  updateOrganization(id: Int, name: String, description: String): Organization
  removeOrganization(id: Int): Boolean
}

Resolver GraphQL

@Resolver('Organization')
export class OrganizationResolver {
  Constructor( //En el constructor recibe las dependencias
    @Inject('Organization') private model: OrganizationModel
  ){}
}

Resolver GraphQL

@Resolver('Organization')
export class OrganizationResolver {
  @Query('organization')
  async getOrganization(_, args) {
    const { id } = args;
    return await this.model.findOneById(id);
  }
}

Resolver GraphQL

@Resolver('Organization')
export class OrganizationResolver {
  @Query('allOrganization')
  async getAllOrganization() {
    return await this.model.find();
  }
}

Resolver GraphQL

@Resolver('Organization')
export class OrganizationResolver {
  @Mutation()
  async createOrganization(_, organization: Organization) {
    return await this.model.save(organization);
  }
}

Resolver GraphQL

@Resolver('Organization')
export class OrganizationResolver {
  @Mutation()
  async updateOrganization(_, organization: Organization) {
    await this.model.save(organization);
    return await this.model.findOneById(organization.id);
  }
}

Resolver GraphQL

@Resolver('Organization')
export class OrganizationResolver {
  @Mutation()
  async removeOrganization(_, args) {
    const { id } = args;
    await this.model.deleteById(id);
    return true;
  }
}

Clase Módulo

@Module({
  imports: [TypeOrmModule.forFeature([Organization])], //Clases que debe gestionar TypeOrm
  components: [
    ProxyModel.injectorFactory('Organization'), //Model de organizacion
    OrganizationModel,
    OrganizationResolver, //Resolver de graphql
  ],
  controllers: [OrganizationApi], //Controller de Api Rest
})
export class OrganizationModule {
  configure(consumer: MiddlewaresConsumer) {
    JwtMiddlewaresApply(consumer, OrganizationApi); //Definir que para OrganizationApi se debe estar autenticado
  }
}

Seguridad

@Module()
export class OrganizationModule {
  configure(consumer: MiddlewaresConsumer) {
    JwtMiddlewaresApply(consumer, OrganizationApi); //Definir que para OrganizationApi se debe estar autenticado
  }
}

En el caso de las Api Rest se marca las controladoras en las que debe estar autenticado el usuario

Seguridad

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { IsString, IsEmpty, IsEmail } from 'class-validator';

@Entity()
export class User {

  @PrimaryGeneratedColumn()
  @IsEmpty()
  id: number;

  @Column({ length: 500 })
  @IsEmail()
  email: string;

  @Column({ length: 500 })
  @IsString()
  password: string;

  @Column('simple-array')
  @IsEmpty()
  roles: string[];

  plainPassword: string;
}

Seguridad

Endpoint para autenticar

Post: /api/auth/token.

Entrada:
{
  email: string,
  password: string,
}
Salida:
{
     expires_in: number,
     access_token: string,
}

Seguridad

En el fichero “src/roles.ts” se ponen los roles que se va usar y los roles por defecto que tendrá un usuario.

export enum Rol {
  USER = 'USER',
  ADMIN = 'ADMIN',
}
export const DefaultRoles = [Rol.USER];

Seguridad

El acceso por Rol a la Api Rest o GraphQL se puede configurar a nivel de clase o a nivel de método.

@Controller('organization')
@Roles(Rol.USER)
export class OrganizationApi {

  @Get()
  async list(): Promise<Organization[]> {
    return await this.model.find();
  }

  @Post()
  @Roles(Rol.ADMIN)
  async create(@Body(new ValidationPipe()) organization: Organization) {
    return await this.model.save(organization);
  }
}

Pruebas automáticas

  • Se harán dos tipos de pruebas e2e y unit test.
  • Las pruebas se pondrán en la carpeta “test” y dentro habrá una carpeta para cada tipo de prueba.
  • La estructura de carpeta de las pruebas será la misma que en la carpeta “src”.
  • Se propone hacer solo pruebas e2e para las clases relacionadas con el negocio y pruebas unitarias para las clases de soporte.

NestJs Framework

By product@dailybot.com

NestJs Framework

  • 1,363