NestJs - alandrade21/docsCompartilhados GitHub Wiki


Nest - Instalação

Cli:

Recomenda-se instalar o cli em escopo global.

npm i -g @nestjs/cli

nest new <nome> cria um novo projeto.

Starter project from github:

Faça o git clone do [starter project](Faça o git clone do starter project).

git clone https://github.com/nestjs/typescript-starter.git

Rodando o Projeto

npm run start:dev

O projeto sobe em localhost:3000.

Modules

Pacote de funcionalidades (controladores, serviços, entidades, etc.).

Se o módulo chama users, seu entry point é users.module.ts.

Possui um controlador que roteia todas as requisições (api end points), de nome users.controller.ts.

Ainda pode ter arquivos para service, entity e spec.

O módulo padrão é o app.module.ts que está conectado ao bootstrap da aplicação, que é o arquivo main.ts (ambos na raiz de /src).

Todos os demais módulos devem estar conectados ao ap-p module para serem entendidos pelo nest.

Para criar um module com o CLI:

nest g module <nome>

O módulo é um encapsulador. Tudo criado dentro de um módulo não é visível fora dele. Para tornar elemento criados dentro de um módulo visíveis fora dele é preciso que o módulo export esses elementos.

O módulo também controla visibilidade dentro dele. Para que um elemento criado dentro de um módulo enxergue outros elementos criados dentro do mesmo módulo, é preciso que o módulo provide esses elementos.

Request Response Lifecycle

Ciclo de vida das requisições no nest

Controllers

Onde são criados os end points.

nest g controller <nome>

Tem que ser inserido no array de controladores do @Module. O comando acima já faz isso.

Parâmetros e Queries

Parâmetro obrigatório:

@Get('/:id/')
public getUser(@Params() params: any) {}

Parâmetro opcional:

@Get('/{:id}/')
public getUser(@Params() params: any) {}

Todos os parâmetros obrigatórios tem que vir antes dos opcionais.

Vários parâmetros:

@Get('/:param1/:param2')
public getUser(@Params() params: any) {}

Queries:

@Get()
public getUser(@Query() query: any) {}

Acima, params e query são objetos js.

Para pegar o request inteiro, usa:

@Post()
public getUser(@Req() request: Request) {}

Onde @Req é do nest e Request é do express. Mais útil para debug.

Pegando um parâmetro específico:

@Get('/:param1/:param2')
public getUser(
  @Param('param1') param1: any,
  @Param('param2') param2: any,
) {}

@Headers pega os cabeçalhos. @Ip pega o endereço ip do sender.

Wrap up:

@Get('/:id/{:optional}')
public getUser(
  @Param('id', ParseIntPipe) id: number | undefined, // undefined se for optional e ausente
  @Param('optional') optional?: number | undefined,
  @Query('limit', parseIntPipe) limit?: number,
){}

Pipes - Validação e Transformação de Parâmetros e Queries

São executados logo antes da requisição chegar no controller.

São úteis para escrever código de validação dos dados da requisição e de transformação de dados da request.

A lista de pipes internos do nest está na página pipes da documentação.

Um pipe de transformação/validação é usado como parâmetro de um decorator. Por exemplo:

@Param('id', ParseIntPipe) id: number

Nesse caso, se a validação falhar, volta um erro 400 com uma mensagem de falha de validação, por padrão.

DefaultValuePipe atribui um valor default para um elemento:

@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number

Validação de DTOs

Usar o pacote class-validator. A documentação pode ser encontrada noi npm e no github(https://github.com/typestack/class-validator). Tudo o que essa lib faz é suportado pelo nest.

npm i class-validator
export class UserDto {

  @IsOptional()
  @IsInt()
  id?: number | undefined;

  @IsString()
  @IsNotEmpty()
  @MinLength(3)
  firstName: string;

  @IsString()
  @IsOptional()
  @MinLength(3)
  lastName?: string | undefined;

  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsEnum(TipoUsuarioEnum)
  @IsNotEmpty()
  tipo: TipoUsuarioEnum;

  @IsJSON()
  @IsOptional()
  valorJson?: string;

  @IsUrl()
  @IsOptional()
  url?: string;

  @IsISO8601()
  dataCadastro: Date;

  @IsArray()
  @IsString({each: true})
  @MinLength(3, {each: true})
  @IsOptional()
  tags?: string[];

  @IsString()
  @IsString()
  @MinLength(8)
  @Matches(regex, {
    message: 'mensagem customizada'
  })
  password: string;

  @IsArray()
  @ValidateNested({each: true}) // Essas duas linhas são para validar nested DTOs
  @Type(() => OutrasInfosDTO)
  outrasInfos: OutrasInfosDTO[];
}

O DTO é criado no arquivo user.dto.ts.

Body

Para ligar DTOs ao body, usa o pacote class-transform.

npm i class-transform
@Post()
public createUser(@Body(new ValidationPipe()) userDto: UserDto) {}

Nesse caso userDto será um object, não uma instância de UserDto.

Ao invés de fazer isso em todo DTO em todo serviço, dá pra ligar a validação globalmente no main.ts:

app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // Se vier algum campo no body não presente no DTO, remove o campo.
      forbidNonWhitelisted: true, // Dá erro se vierem campos adicionais.
      transform: true, // transforma o dto recebido numa instância do tipo fornecido;
      transformOptions: {
        enableImplicitConversion: true, // Pra não ter que usar @Type
      }
    }),
);

Providers e Dependency Injection

Intra-Module Dependency

Um serviço precisa ser declarado com o decorator @Injectable().

@Injectable
export class MeuServico {}

O serviço é criado no arquivo meuServico.service.ts.

No módulo que precisa usar esse serviço, o serviço precisa ser inserido no array de providers desse módulo:

@Module({
  imports: [],
  controllers: [AppController],
  providers: [MeuServico],
})
export class AppModule {}

Para usar o serviço, basta colocá-lo como parâmetro no construtor do controller.

@Controller()
export class AppController {
  constructor(private readonly meuServico: MeuServico) {}

  ...
}

Criando um serviço via CLI:

nest g service <nome>

Inter-Module Dependency

Imagine um serviço ServiceA, criado dentro de um ModuleA, que precise ser consumido por um ServiceB, criado em um ModuleB.

Assim, ModuleA precisa exportar ServiceA:

@Module({
  controllers: [ControllerA],
  providers: [ServiceA],
  exports: [ServiceA],
})
exports class ModuleA {}

Para acessar os serviços exportados por ModuleA, ModuleB precisa importar ModuleA:

@Module({
  controllers: [ControllerB],
  providers: [ServiceB],
  imports: [ModuleA],
})
exports class ModuleB {}

Agora ServiceB pode injetar ServiceA:

@Injectable()
exports class ServiceB {
  constructor(private readonly serviceA: ServiceA) {}

  ...
}

Circular Dependencies

Dão um erro A circular dependency between modules. no terminal. Para evitar o problema usa forwardRef:

@Module({
  controllers: [ControllerA],
  providers: [ServiceA],
  exports: [ServiceA],
  imports: [forwardRef(() => ModuleB)],
})
exports class ModuleA {}

@Module({
  controllers: [ControllerB],
  providers: [ServiceB],
  exports: [ServiceB],
  imports: [forwardRef(() => ModuleA)],
})
exports class ModuleB {}

@Injectable()
exports class ServiceB {
  constructor(
    @Inject(forwardRef(() => ServiceA))
    private readonly serviceA: ServiceA
  ) {}

  ...
}

@Injectable()
exports class ServiceA {
  constructor(
    @Inject(forwardRef(() => ServiceB))
    private readonly serviceB: ServiceB
  ) {}

  ...
}

Usando Classes Abstratas como Providers

@Module({
  controllers: [ControllerA],
  providers: [
    ServiceA,
    {
      provide: AbstractProvider,
      useClass: ImplementationProvider,
    }
  ],
  ...
})
exports class ModuleA {}

Documentação OpenAPI

Documentação completa na página de documentação do nest.

Usa swagger:

npm i @nestjs/swagger

A configuração é feita no main.ts:

const config = new DocumentBuilder()
  .setTitle('Título da Aplicação")
  .setVersion('1.0')
  .setDescription('Descrição')
  .setTermsOfService('URL para página de termos de serviço')
  .setLicense('Nome da Licença', 'Link para a licença')
  .addServer('Link para acessar a documentção gerada, ex localhost:3000/api')
  .build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document); // api é o endpoint onde estará a documentação.

A documentação vai responder em localhost:3000/api.

Nos controladores, abaixo do @Controller(), coloca um @ApiTags('Nome da Tag') para colocar todas as entradas desse controlador em baixo de uma tag específica na página de documentação.

Documentando os End Points

@Get('/:id/{:optional}')
@ApiOperation({
  summary: '',
})
@ApiQuery({
  name: 'limit',
  type: 'number',
  required: false,
  description: '',
  example: 10,
})
@ApiQuery({...}) // segunda query
@ApiResponse({
  status: 200,
  description: '',
})
@ApiResponse({ // Pode ter vários responses, com todos os retornos possíveis.
  status: 422,
  description: '',
})
public getUser(
  @Param('id', ParseIntPipe) 
  id: number | undefined, // undefined se for optional e ausente

  @Param('optional') 
  @ApiPropertyOptional({
    description: '',
    example: 1234
  })
  optional?: number | undefined,

  @Query('limit', parseIntPipe) 
  limit?: number,
){}

Documentando DTOs

export class UserDto {

  @ApiPropertyOptional({ // Todas as propriedades opcionais são decoradas com esse
    description: '',
    example: '',
  })
  @IsOptional()
  @IsInt()
  id?: number | undefined;

  @ApiProperty({ // Todas as propriedades obrigatórias são decoradas com esse
    description: '',
    example: '',
  })
  @IsString()
  @IsNotEmpty()
  @MinLength(3)
  firstName: string;

  ...
 
  @ApiProperty({
    enum: TipoUsuarioEnum,
    description: 'Bom listar os valores possíveis',
    example: '',
  })
  @IsEnum(TipoUsuarioEnum)
  @IsNotEmpty()
  tipo: TipoUsuarioEnum;

  @ApiProperty({
    type: 'array',
    items: {
      type: 'object',
      properties: {
        // aqui lista as propriedades do nested dto (na mão?)
      }
    }
    description: 'Bom listar os valores possíveis',
    example: '',
  })
  @IsArray()
  @ValidateNested({each: true}) // Essas duas linhas são para validar nested DTOs
  @Type(() => OutrasInfosDTO)
  outrasInfos: OutrasInfosDTO[];
}

Documentação JSDocs

Similar a java docs.

A documentação dos decorators está na página oficial.

TypeORM

TypeORM - Instalação

Tem que instalar os pacotes typeorm e @nestjs/typeorm.

Além disso, para o typeORM funcionar é necessário instalar o driver do banco a ser utilizado.

A documentação do typeorm pode ser encontrada em typeorm.io. A instalação dos drivers de banco pode ser rncontrada na seção de Drivers da árvore lateral.

TypeORM - Configuração

A configuração é feita no app.module.ts, colocando o TypeOrmModule do nest no array de imports do módulo principal da aplicação.

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [],
      inject: [],
      useFactory: () => ({
        type: "postgres",
        entities: [__dirname + "/../../**/*.entity{.ts,.js}"],
        autoLoadEntities: true // Carregas todas as entidades. Desliga o de cima e liga esse.
        synchronize: true, // Set to false in production
        host: "localhost",
        port: 5432,
        username: "your_username",
        password: "your_password",
        database: "your_database",
      }), 
    }),
  ],
  ...
})
...

Repository Pattern

A entidade está num arquivo nome.entity.ts. Anotações similares a hibernate.

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @column()
  name: string;

  @column({default: true})
  isActive: boolean;
}

Na camada de services, o repository é injetado:

@Injectable()
export class UsersService {
  constructor (
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  async findAll(): Promise<User[]> {
    await this.userRepository.find();
  }
}

Para essa injeção funcionar, no módulo de usuários é necessário importar o seguinte:

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
  imports: [TypeOrmModule.forFeature([User])]
})
exports class UsersModule {}

Exemplo de inserção de novas entidades.

public async createUser(createUserDto: CreateUserDto) {
  const existingUser = await this.usersRepository.findOne({
    where: {email: createUserDto.email},
  });

  ...

  let newUser = this.userRepository.create(createUserDto); // Cria a entidade a partir do dto.
  newUser = this.userRepository.save(newUser);

  return newUser;
}

Relacionamentos

1-N

@Entity()
export class Livro {
  ...
  @ManyToOne(() => Autor, (autor) => autor.livros, {eager: true})
  autor: Autor; // A FK fica desse lado.
  ...
}

@Entity()
export class Autor {
  ...
  @OneToMany(() => Livro, (livro) => livro.autor, {onDelete: 'CASCADE'})
  livros: Livro[];
  ...
}

Cascade

  • @OneToMany(() => Livro, (livro) => livro.autor, {cascade: 'remove'})

Essa forma é resolvida pelo typeORM.

Pode ser true, "insert", "update", "remove", "soft-remove" ou "recover".

True faz cascade all.

Pode ser um array: cascade: ['remove', 'insert'].

  • @OneToMany(() => Livro, (livro) => livro.autor, {onDelete: 'CASCADE'})

Essa forma é resolvida pelo banco.

Também há a opção onUpdade ({onDelete: 'CASCADE', onUpdate: 'CASCADE'}).

Os valores válidos são: 'CASCADE', 'SET NULL', 'RESTRICT' ou 'NO ACTION', 'SET DEFAULT' (default definido na coluna FK).

Fetch

Ao buscar os dados de um Livro, a associação com Autor não é buscada por default.

A primeira forma de buscar as entidades associadas é via serviço:

public async buscaLivro(id: number) {
  let livro = await this.livroRepository.findOneBy({
    id,
    relations: {
      autor: true,
    }
  });
}

A segunda forma é via configuração da associação na entidade:

@ManyToOne(() => Autor, (autor) => autor.livros, {eager: true})

N-N Unidirecional

@Entity()
export class Livro {
  ...
  @ManyToMany(() => Tag)
  @JoinTable()
  tags: Tag[];
  ...
}

Nesse tipo de relacionamento o cascade delete do livro para a tabela de relacionamento sempre está ligado. Então, ao apagar um livro, todos os relacionamentos desse livro com tags serão apagados.

N-N Bidirecional

@Entity()
export class Livro {
  ...
  @ManyToMany(() => Tag, (tag) => tag.posts, {eager: true})
  @JoinTable()
  tags: Tag[]; // A FK fica desse lado.
  ...
}

@Entity()
export class Tag {
  ...
  @ManyToMany(() => Livro, (livro) => livro.tags, {onDelete: 'CASCADE'})
  livros: Livro[];
  ...
}

Nesse tipo de relacionamento o delete cascade não está ligado no lado que não tem a @JoinTable. Nesse caso é necessário definir o cascade no relacionamento.

1-1 Unidirecional

@Entity()
export class EntidadeA {
  ...
  @OneToOne(() => EntidadeB, {cascade: true, eager: true})
  @JoinColumn() // Coloca na EntidadeB uma FK para EntidadeA
  entidadeB: EntidadeB;
  ...
}

Com esse cascade, o método create do repositório (que transforma um dto em entidade) também faz a transformação dos DTOs associados.

1-1 Bidirecional

@Entity()
export class EntidadeA {
  ...
  @OneToOne(() => EntidadeB, (entidadeB) => entidadeB.entidadeA, {cascade: true, eager: true})
  entidadeB: EntidadeB;
  ...
}

@Entity()
export class EntidadeB {
  ...
  @OneToOne(() => EntidadeA, (entidadeA) => entidadeA.entidadeB)
  @JoinColumn() // Coloca na EntidadeB uma FK para EntidadeA
  entidadeA: EntidadeA;
  ...
}

Query Language

Documentação oficial.

Shortcuts de Sintaxe

Vários métodos aceitam alguns encurtamentos de sintaxe, por exemplo:

public async delete(id: number) {
  let entidadeA = await this. entidadeARepository.findOneBy({id: id});
  ...
}

pode ser escrito:

public async delete(id: number) {
  let entidadeA = await this. entidadeARepository.findOneBy({id});
  ...
}

Find

let entidades = await this.entidadeRepository.find({
  where: {
    id: In(array),
  }
});

Controle de paginação está aqui.

Soft Delete

await this.entidadeRepository.softDelete(id);

Ao invés de remover, coloca uma data em um campo que indica a data em que a remoção lógica ocorreu.

Para funcionar, um dos campos de data da entidade tem que estar decorado com @DeleteDateColumn().

Veja também os decoradores @CreateDateColumn() e @UpdateDateColumn().

Controle Transacional

O controle transacional é feito a partir de um objeto QueryRunner que necessita de um datasource, que é obtido do TypeORM:

@Injectable()
export class MeuServico {

  constructor(
  
    private readonly dataSource: DataSource, // from TypeORM
  )

  public async servicoTransacional(...) {
    // Create Query Runner instance
    const queryRunner = this.dataSource.createQueryRunner();

    // Connect Query Runner to the data source
    await queryRunner.connect();

    // Start transaction
    await queryRunner.startTransaction();

    try {
      let newEntidade = queryRunner.maanger.create(Entide, entidade);
      let result = await queryRunner.manager.save(newEntidade);

      ...

      await queryRunner.commitTransaction();

    } catch (error) {

      await queryRunner.rollbackTransaction();

    } finally {

      await queryRunner.release(); // Release connection.
  }
}

Uma forma mais parecida com spring está neste artigo, usando typeorm-transactional-service com documentação no github

Há uma maneiras de controlar transações com entity manager, onde não precisa dar commit na mão. Procurar e estudar isso depois.

Environments e Config Module

Instalação

Instala o config module de @nestjs/config.

No app.module.ts:

@Module({
  imports: [
    ConfigModule.forRoot({
      // Isso faz com que não tenha que injetar esse módulo em todos os módulos do sistema.
      isGlobal: true, 
    })
  ]
})
export class AppModule implements NestModule {}

Arquivo de configuração

As variáveis de ambiente são, por default, carregadas de um arquivo .env.

É uma boa prática definir uma variável de ambiente NODE_ENV com o valor development no script de inicialização de desenv:

"start:dev": "NODE_ENV=development nest start --watch",

Depois, é uma boa prática carregar arquivos diferentes com base no valor dessa variável, por exemplo,em desenvolvimento, carregar um arquivo .development.env.

const ENV = process.env.NODE_ENV;

@Module({
  imports: [@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobexport class AppModule implements NestModule {}
```al: true, 
      envFilePath: !ENV ? '.env' : '.${ENV}.env',
    })
  ]
})
export class AppModule implements NestModule {}

A partir do momento que o ConfigModule foi configurado, não utilize mais process.env para acessar variáveis de ambiente, utilize o config.service.

Acessando Variáveis de Ambiente

Dentro de um .forRoot tem que importar o ConfigModule e injetar o ConfigService:

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true, 
      envFilePath: !ENV ? '.env' : '.${ENV}.env',
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: "postgres",
        entities: [__dirname + "/../../**/*.entity{.ts,.js}"],
        autoLoadEntities: true // Carregas todas as entidades. Desliga o de cima e liga esse.
        synchronize: true, // Set to false in production
        host: configService.get('DATABASE_HOST'),
        port: +configService.get('DATABASE_PORT'),
        ...
      }), 
    })
  ]
})
export class AppModule implements NestModule {}

configService.get acessa as variáveis de ambiente por nome.

Se alguma das propriedades precisar ser convertida para um número, basta colocar um + na frente do método: +configService.get

Validando Variáveis de Ambiente

Instala o pacote joi.

Cria um arquivo environment.validation.ts:

import * as joi from 'joi';

export default Joi.object({
  NODE_ENV: joi.string()
    .valid('development', 'production')
    .default('development'),
  DATABASE_PORT: joi.number().port().default(5432),
  DATABASE_PASSWORD: joi.string().required(),
});

Para usar isso, dentro do forRoot do ConfigModule:

import environmentalValidation from <caminho para o arquivo environment.validation.ts>

...

ConfigModule.forRoot({
  isGlobal: true, 
  envFilePath: !ENV ? '.env' : '.${ENV}.env',
  validationSchema: environmentValidation,
}),

Exception Handling

alt text

Dentro do filter boundary da imagem acima, nest captura exceções e as transforma em response (exception filters).

As exceções a serem lançadas no código estão listadas na documentação oficial.

Custom exceptions podem ser criadas com um throw new HttpException.

throw new HttpException(
  {
    status: HttpStatus.MOVED_PERMANENTLY,
    error: 'Mensagem de erro',
    //outros campos que sejam interessantes nesse contexto...
  },
  HttpStatus.MOVED_PERMANENTLY,
  {
    description: 'informações sensíveis a serem logadas e não enviadas para o usuário'
    // outros campos a serem logados
  }
);
⚠️ **GitHub.com Fallback** ⚠️