NestJs - alandrade21/docsCompartilhados GitHub Wiki
- Nest - Instalação
- Rodando o Projeto
- Modules
- Request Response Lifecycle
- Controllers
- Providers e Dependency Injection
- Documentação OpenAPI
- Documentação JSDocs
- TypeORM
- Environments e Config Module
- Exception Handling
Cli:
Recomenda-se instalar o cli em escopo global.
npm i -g @nestjs/clinest 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.gitnpm run start:devO projeto sobe em localhost:3000.
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.

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â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,
){}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: numberNesse 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: numberUsar 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-validatorexport 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.
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
}
}),
);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>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) {}
...
}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
) {}
...
}@Module({
controllers: [ControllerA],
providers: [
ServiceA,
{
provide: AbstractProvider,
useClass: ImplementationProvider,
}
],
...
})
exports class ModuleA {}Documentação completa na página de documentação do nest.
Usa swagger:
npm i @nestjs/swaggerA 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.
@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,
){}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[];
}Similar a java docs.
A documentação dos decorators está na página oficial.
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.
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",
}),
}),
],
...
})
...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;
}@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[];
...
}@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).
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})
@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.
@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.
@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.
@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;
...
}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});
...
}let entidades = await this.entidadeRepository.find({
where: {
id: In(array),
}
});Controle de paginação está aqui.
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().
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.
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 {}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.
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
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,
}),
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
}
);