API NestJs + Swagger + Docker Compose
Sobre o projeto.
Esse projeto foi construido com a finalidade de aprofundamento de tecnicas com NestJs, utilizaremos tecnicas do Docker Compose para criar nosso banco de dados padrão e documentação via Swagger. O projeto trata-se de um CRUD para cadastro de itens, consulta e atualização. Algo bem simples apenas exemplificar a construção e as tecnicas do NestJs.
A versão Node utilizada foi a Node.js (>= 18.7.0).
Inicialização do projeto
O primeiro passo foi a inicialização de um novo projeto com a CLI do proprio NestJs, executando o comando:
$ nest new nestjs-api
Caso não deseje instalar a CLI globalmente podemos executar o comando:
$ npx nest new nestjs-api
Ao executar um dos comandos acima teremos a lista de arquivos criados e o gerenciador de pacotes de sua preferencia (npm ou yarn). Por escolha pessoal utilizei o npm neste projeto.
Após inicializar o projeto podemos ver no terminal os logs de inicialização.
Adicionando dependências
Antes que comecemos de fato a modificação de codigo vamos instalar algumas dependências como o TypeORM e o PostgreSQL para gerenciar o nosso banco de dados.
$ npm install --save @nestjs/typeorm typeorm pg
Vamos adicionar também a dependencia de ModuleConfig, responsável por carregar as declarações do arquivo .env nas variaveis de ambiente.
$ npm install --save @nestjs/config
Internamente o NestJs utiliza o dotnev, por isso não precisaremos instalar manualmente a dependência.
Por se tratar de um sistema desenvolvido apenas para estudo, vamos manter o arquivo .env no repositório, mas por praticas de segurança não esqueça de adicionar o .env em seu .gitignore para não expor seus acessos a pessoas má intencionadas.
$ npm install --save class-validator class-transformer
A dependência acima será utilizada para garantir que os valores recebidos por nossos DTOs (veremos a frente) estejam de acordo com o que esperamos. Podemos verificar a lista completa de validações na documentação oficial
Vamos também realizar a documentação dessa api via Swager.
$ npm install --save @nestjs/swagger swagger-ui-express
Banco de dados
Como vimos acima vamos utilizar o banco de dados PostgreSQL + Docker Compose para esse projeto.
Não adentraremos muito na teoria do Docker Compose pois o foco é o NestJs, mais informações temos a documentação oficial.
Iniciaremos criando um container para o PostgreSQL, que vai rodar em um container próprio. Para isso, criamos um arquivo chamado docker-compose.yaml na pasta raiz do projeto.
No arquivo YAML vamos declarar:
version: '3.8'
services:
db:
image: postgres
container_name: nest-js-db
tty: true
environment:
- POSTGRES_DB=nest-js
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=root
volumes:
- nest-js-pgdata:/var/lib/postgresql/data
ports:
- '5432:5432'
expose:
- 5432
networks:
- portal-network
networks:
portal-network:
driver: bridge
external: true
volumes:
nest-js-pgdata:
external: true
- version: a versão do formato do arquivo Docker Compose. Segue uma especificação definida pela própria Docker e depende da versão do Docker Runtime. Para mais informações, consulte a documentação oficial.
- services: aqui segue a lista de containers da nossa aplicação. Para cada container, uma série de parâmetros deverão ser informados. Os parâmetros vão variar conforme o container que está sendo criado.
- image: todo container criado com Docker tem uma imagem de base. Imagens são como templates para a construção de containers. Utilizamos uma imagem oficial do Postgres, na versão 14, com uma tag alpine. Imagens alpine são bastante enxutas e de tamanho reduzido, sendo, portanto, adequadas para deploy de aplicações, principalmente em produção.
- container_name: nome do container a ser criado e a partir do qual poderemos referenciá-lo nos comandos de cli do Docker.
- ports: as portas do container listadas aqui serão compartilhadas com os serviços descritos no arquivo docker-compose.yml e com o host. É possível especificar uma porta do host para fazer binding. Caso não seja especificada uma porta do host, uma randômica será utilizada. No nosso exemplo, a porta 5432 do container estárá ligada à porta 5432 do host.
- volumes: containers, por concepção, não devem conter dados persistentes. Containers que precisem persistir dados devem especificar volumes, que são áreas de armazenamento do host mapeadas e montadas no container. Arquivos dessas pastas não serão alterados ou removidos depois que os containers forem desligados (exceção para o comando docker-compose down --volumes)
Agora com as configurações prontas, rodamos o comando para subir o container:
$ docker compose up -d
Criando a estrutura de entidades
O framework do NestJs fornece uma serie de scripts prontos para a criação de estruturas completas de CRUDs, microserviços e etc.
Utilizando o NestCLI's CRUD para criar automaticamente a entidade, módulo, controlador REST, services e DTO's asssim como os arquivos .spec para os testes.
$ nest g resource item
Ao executar, devemos escolher a camada de transporte para o nosso recurso, no caso deste prjeto vamos utilizar a API REST, em seguida, você gostaria de gerar pontos de entrada CRUD? (Y / n)?, entramos com Y e Enter.
Após isso vemos que a pasta item foi criada no nosso diretorio src com nossos arquivos padroes do CRUD e também a pasta entities e dto.
Agora podemos finalmente definir nossa entidade item:
import {
BaseEntity,
Column,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
export class Item extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
updated_at: Date;
@Column({ name: 'name', type: 'varchar', length: 50 })
name: string;
@Column({ name: 'description', type: 'varchar', length: 255, nullable: true })
description?: string;
@Column({ name: 'quantity', type: 'int', default: 1 })
quantity: number;
}
Observe que no arquivo item.entity.ts temos as definições de mapeamento feitas todas por Notations do TypeOrm.
No exemplo da coluna de descrição, só precisariamos declarar como @Column({ nulo: true }) pois se trata de um dado opcional, mas preenchemos todas as informações para obter mais detalhes.
Para o updateAt, foi utilizado o decorator @UpdateDateColumn, que atualiza o valor automaticamente, sempre que uma atualização é feita no registro.
Se inicializarmos o aplicativo a tabela será criada automaticamente em nosso banco de dados, com a estrutura definida acima, devido a sincronização ativada no AppModule (process.env.TYPEORM_SYNCHRONIZE).
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.TYPEORM_HOST,
port: parseInt(process.env.TYPEORM_PORT),
username: process.env.TYPEORM_USERNAME,
password: process.env.TYPEORM_PASSWORD,
database: process.env.TYPEORM_DATABASE,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: process.env.TYPEORM_SYNCHRONIZE === 'true',
}),
ItemModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Difinindo o DTO (Data Transfere Object)
Data Transfer Object (DTO) ou simplesmente Transfer Object é um padrão de projetos bastante usado em Java para o transporte de dados entre diferentes componentes de um sistema, diferentes instâncias ou processos de um sistema distribuído ou diferentes sistemas via serialização.
Não podemos expor diretamente nosso modelo interno ( do banco de dados ) àqueles que consomem a API, mas sim uma representação dos dados com os atributos relevantes ( ou permitidos ) para uso externo. Ter maior controle sobre os dados e a possibilidade de um melhor desempenho ( consultando apenas as colunas necessárias da tabela ) são algumas das vantagens do uso de DTOs. Então, vamos defini-los no arquivo create-item.dto.ts:
import { IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';
export class CreateItemDto {
@IsString()
@IsNotEmpty()
name: string;
@IsOptional()
@IsString()
description: string;
@IsInt()
@Min(0)
quantity: number;
}
O arquivo de updateItemDto não será preciso nenhuma alteração pois o mesmo utilizará como base nosso CreateItemDto através da notation PartialType(CreateItemDto) que observará as propriedades e as transformará em opcionais.
Agora, atualizaremos nosso arquivo main.ts para que execute a validação automatica, adicionando a linha app.useGlobalPipes(new ValidationPipe());
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(process.env.SERVER_PORT);
}
bootstrap();
Implementando ItemService
Utilizando o padrão Repository, vamos criar uma abstração para maneira como obtemos os dados para a entidade. Como no caso do item, temos um CRUD simples, vamos usar o Repository fornecido pelo TypeORM:
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateItemDto } from './dto/create-item.dto';
import { UpdateItemDto } from './dto/update-item.dto';
import { Item } from './entities/item.entity';
@Injectable()
export class ItemService {
constructor(
@InjectRepository(Item) private readonly repository: Repository<Item>,
) {}
create(createItemDto: CreateItemDto): Promise<Item> {
const item = this.repository.create(createItemDto);
return this.repository.save(item);
}
findAll(): Promise<Item[]> {
return this.repository.find();
}
findOne(id: string): Promise<Item> {
return this.repository.findOne(id);
}
async update(id: string, updateItemDto: UpdateItemDto): Promise<Item> {
const item = await this.repository.preload({
id: id,
...updateItemDto,
});
if (!item) {
throw new NotFoundException(`Item ${id} not found`);
}
return this.repository.save(item);
}
async remove(id: string) {
const item = await this.findOne(id);
return this.repository.remove(item);
}
}
Como estamos usando um provedor externo, precisamos declarar seu modulo como uma importação de item.module.ts, deixando assim:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Item } from './entities/item.entity';
import { ItemController } from './item.controller';
import { ItemService } from './item.service';
@Module({
imports: [TypeOrmModule.forFeature([Item])],
controllers: [ItemController],
providers: [ItemService],
})
export class ItemModule {}
Por fim, com as novas alterações devemos modificar o arquivo item.controller.ts substituindo o +id
apenas por id
.
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { ItemService } from './item.service';
import { CreateItemDto } from './dto/create-item.dto';
import { UpdateItemDto } from './dto/update-item.dto';
@Controller('item')
export class ItemController {
constructor(private readonly itemService: ItemService) {}
@Post()
create(@Body() createItemDto: CreateItemDto) {
return this.itemService.create(createItemDto);
}
@Get()
findAll() {
return this.itemService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.itemService.findOne(id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateItemDto: UpdateItemDto) {
return this.itemService.update(id, updateItemDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.itemService.remove(id);
}
}
Agora nossa api está finalmente implementada.
Documentando a Api.
O Swagger é uma maneira simples e pratica para se documentar APIs de acordo com a OpenApi.
Vamos atualizar nosso modulo main.ts para ficar assim:
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
const config = new DocumentBuilder()
.setTitle('Lista de itens')
.setDescription('Minha lista de itens para compras')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
Agora, precisamos apenas atualizar nossas entidades e os DTOs com os decoratos para sejam identificados pelo Swagger.
Em nosso DTO create-item.dto.ts vamos adicionar as importações de atributos @ApiProperty(). Quando a variavel for opcional podemos usar o @ApiPropertyoptional(). O arquivo ficará assim:
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';
export class CreateItemDto {
@ApiProperty({ example: 'Bananas' })
@IsString()
@IsNotEmpty()
name: string;
@ApiPropertyOptional({
example: 'bananas',
description: 'Optional description of the item',
})
@IsOptional()
@IsString()
description: string;
@ApiProperty({ example: 5, description: 'Needed quantity' })
@IsInt()
@Min(0)
quantity: number;
}
Já para o caso o update-item.dto.ts por utilizar as especificações de dentro de nosso create-item.dto.ts ja temos a injeção dos decorators dentro dele, apenas precisamos trocar a importação do ParcialType para que venha de dentro do modulo do swagger e esta tudo pronto. O arquivo ficará assim:
import { PartialType } from '@nestjs/swagger';
import { CreateItemDto } from './create-item.dto';
export class UpdateItemDto extends PartialType(CreateItemDto) {}
Para visualizar nossa documentação feita pelo swagger devemos inicializar a aplicação com:
$ npm run start:dev
e em nosso navegador acessamos pelo endereço: http://localhost:3000/api. Para saber mais como utilizar a documentação e testar nossas requisições podemos consultar a documentação do próprio modulo Swagger dentro do NestJs
Referencias e Agradecimentos
O projeto de estudo foi montado seguindo as documentações oficiais e tutoriais a baixo, o credito é todo deles, sou apenas um Dev em desenvolvimento e devo tudo a eles. ;D
Este é apenas um ponto de partida pequeno dentro do NestJs, nele podemos ver quão simples e facil é a criação de apis com boas praticas de escrita, organização e agilidade de processo com ajuda da NestCLI.
- Data Transfer Object
- Creating an API with NestJS - SideChannel
- Brincando com o PostgreSQL - jtemporal
- Postgres + Docker Compose
- Swager
Obrigado por chegarem até aqui, eu sou Tiago Landim e esse é o primeiro de muitos !!