Por Alex Carneiro

Introdução

Atualmente, para criar aplicações frontend utilizando JavaScript, os frameworks Angular, React e Vue destacam-se pela maturidade e popularidade. Além disso, existem muitos exemplos e casos de uso disponíveis para estudo, o que permite que os desenvolvedores  encontrem exemplos facilmente, para criar rapidamente aplicações testáveis e extensíveis.

No backend, a situação é um pouco diferente. O nome mais conhecido é o Express, que define a si mesmo como “um framework web rápido, sem opinião e minimalista”. Com ele é possível criar uma aplicação rapidamente, com total liberdade para o desenvolvedor escolher sua arquitetura: o modo como vai gerenciar rotas, middlewares, template engines, body parsers, tratamento de erros, banco de dados, etc. Seu minimalismo permite uma alta flexibilidade, mas isso pode se tornar um problema para iniciantes; ou mesmo ao se trabalhar com equipes, já que o código corre o risco de ficar desestruturado.

O NestJS resolve este problema ficando numa camada acima do Express, com uma arquitetura bem definida e pronta para uso. Incluindo o uso de bibliotecas já consolidadas e testadas, diminuindo as decisões para os iniciantes, amenizando inconsistências no código e facilitando o entendimento de quem está olhando o projeto pela primeira vez, permitindo criar, assim, aplicações testáveis, escaláveis, fracamente acopladas e fáceis de manter, utilizando filters, pipes, interceptors, entre outros.

A arquitetura, a sintaxe e os componentes são inspirados no Angular. Que apesar de ser voltado a aplicações frontend, possui conceitos arquiteturais que podem facilmente ser utilizados em soluções backend. O NestJS também herda vários conceitos de frameworks que são muito utilizados no meio corporativo, como Spring Boot e .NET Core.

Dentre as vantagens em se utilizar NestJS, temos alguns destaques:

 

Pré-requisitos

Antes de começar a criar nossa API de exemplo, para seguir todos os passos da forma descrita neste guia, você precisa ter instalados:

  • Node.js, versão 10.13 ou superior;
  • Nest CLI (opcional, mas recomendado);
  • PostgreSQL, versão 9 ou superior (pode ser via Docker).

 

Criando nossa API

Neste guia, mostraremos os passos para a criação de uma API RESTful de lista de compras, incluindo as definições dos serviços via Swagger. Vamos começar?

Com o Node.js (>= 10.13), o primeiro passo é instalar a Nest CLI e criar o nosso projeto, executando no terminal:

npm i -g @nestjs/cli
nest new nest-shopping-list

 

Caso não queira instalar a CLI de forma global, é possível executá-la via npx (presente no npm >=5.2). Neste guia, porém, os exemplos serão demonstrados com a CLI instalada:

npx @nestjs/cli new nest-shopping-list

 

Na sequência, você terá a lista de arquivos que foram criados e deverá escolher o gerenciador de pacotes de sua preferência (npm ou yarn). Ao longo deste guia, usaremos o npm como referência.

$ nest new nest-shopping-list
We will scaffold your app in a few seconds…

CREATE nest-shopping-list/.eslintrc.js (631 bytes)
CREATE nest-shopping-list/.prettierrc (51 bytes)
CREATE nest-shopping-list/README.md (3339 bytes)
CREATE nest-shopping-list/nest-cli.json (64 bytes)
CREATE nest-shopping-list/package.json (1980 bytes)
CREATE nest-shopping-list/tsconfig.build.json (97 bytes)
CREATE nest-shopping-list/tsconfig.json (339 bytes)
CREATE nest-shopping-list/src/app.controller.spec.ts (617 bytes)
CREATE nest-shopping-list/src/app.controller.ts (274 bytes)
CREATE nest-shopping-list/src/app.module.ts (249 bytes)
CREATE nest-shopping-list/src/app.service.ts (142 bytes)
CREATE nest-shopping-list/src/main.ts (208 bytes)
CREATE nest-shopping-list/test/app.e2e-spec.ts (630 bytes)
CREATE nest-shopping-list/test/jest-e2e.json (183 bytes)

? Which package manager would you to use? (Use arrow keys)
❯ npm
yarn

 

Confirme sua escolha com Enter, aguarde a instalação dos pacotes e você verá a confirmação da criação do projeto, incluindo as instruções para iniciar a aplicação.

Installation in progress... 

Successfully created project nest-shopping-list
Get started with the following commands:
$ cd nest-shopping-list
$ npm run start

 

A pasta nest-shopping-list foi criada com a estrutura básica do seu novo projeto. Navegue até ela, e faça a inicialização para testar:

cd nest-shopping-list
npm run start:dev

 

Em alguns instantes, você deve ver algo como:

[NestFactory] Starting Nest application...
[InstanceLoader] AppModule dependencies initialized +15ms
[RoutesResolver] AppController {}: +5ms
[RouterExplorer] Mapped {, GET} route +3ms
[NestApplication] Nest application successfully started +3ms

 

Com isso, você já pode testar o servidor abrindo http://localhost:3000 em seu navegador para conferir o “Hello World!” funcionando.

Como iniciamos com npm run start:dev, o servidor foi iniciado com a opção –watch, monitorando os arquivos alterados e recarregando a aplicação automaticamente. Confira mais detalhes e opções na documentação.

Abra o projeto no seu editor de código preferido, como o VSCode, e você verá esta estrutura:

 

No main.ts temos a função bootstrap() que realiza a inicialização da nossa aplicação e começa a escutar na porta 3000.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
 const app = await NestFactory.create(AppModule);
 await app.listen(3000);
}
bootstrap();

 

Você pode abrir o arquivo src/app.service.ts e atualizar o retorno da função getHello() para o que quiser, e salvar. Atualizando a página do navegador, será possível conferir a resposta da API atualizada.

Finalizado este teste inicial, encerre a execução pressionando Ctrl/CMD + C no terminal.

Agora, estamos com tudo pronto, e podemos começar efetivamente a criar sua API!

 

Criando a estrutura para a entidade

Para a nossa lista de compras, teremos a entidade Item, com nome, descrição (opcional) e quantidade de cada item da lista.

Vamos utilizar o CRUD Generator da NestCLI para criar automaticamente nossa entidade, seu módulo, controlador REST, serviço e DTOs, bem como os arquivos .spec de teste.

nest generate resource item

 

Neste momento, deve-se escolher a camada de transporte para nosso recurso: REST, GraphQL, microsserviço ou websocket gateway. Escolha a REST API e para a pergunta “Would you like to generate CRUD entry points? (Y/n)”, digite Y e Enter.

$ nest generate resource item
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/item/item.controller.spec.ts (556 bytes)
CREATE src/item/item.controller.ts (883 bytes)
CREATE src/item/item.module.ts (240 bytes)
CREATE src/item/item.service.spec.ts (446 bytes)
CREATE src/item/item.service.ts (607 bytes)
CREATE src/item/dto/create-item.dto.ts (30 bytes)
CREATE src/item/dto/update-item.dto.ts (169 bytes)
CREATE src/item/entities/item.entity.ts (21 bytes)
UPDATE package.json (2013 bytes)
UPDATE src/app.module.ts (308 bytes)
Packages installed successfully.

 

Confira a nova pasta src/item no seu editor. Lá você irá encontrar os arquivos criados:

  • module.ts: nosso módulo, que especifica os controladores e provedores que são necessários para o funcionamento deste módulo, e estarão disponíveis em seu escopo. Documentação;
  • controller.ts: controlador responsável pelo serviço REST, com os métodos decorados com @Post(), @Get(), @Patch() e @Delete(), todos chamando o itemService. Documentação;
  • service.ts: provedor responsável pela regra de negócio do `Item` e chamadas às fontes de dados. Este serviço também pode ser exposto para que outros módulos da aplicação o utilizem, caso precisem interagir com Item. Documentação;
  • entities/item.entity.ts: nossa entidade Item, que por enquanto está vazia.

 

Adicionando persistência

Antes de começar a alterar o código, vamos instalar as dependências do TypeORM e PostgreSQL para gerenciar nosso banco de dados. Um dos pontos interessantes do TypeORM é que ele pode manter sincronizada a nossa entidade com a estrutura no BD.

npm install --save @nestjs/typeorm typeorm pg

 

Também adicionaremos a dependência do ConfigModule, responsável por carregar as declarações do arquivo .env nas variáveis de ambiente e não manter as propriedades de conexão ao banco e outras informações sensíveis (ou customizadas) direto no código.

npm install --save @nestjs/config

 

Internamente, o @nestjs/config utiliza a popular biblioteca dotenv.

Após as instalações serem concluídas, vamos começar criando nosso arquivo .env na raiz do projeto. Atualize o exemplo, caso as configurações do seu banco PostgreSQL sejam diferentes.

SERVER_PORT=3000
MODE=DEV
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=
DB_DATABASE=shopping_list
DB_SYNCHRONIZE=true

 

Importante

Não esqueça de adicionar uma linha com .env ao arquivo .gitignore, para que ele não fique no repositório acidentalmente.

Deste ponto em diante, os arquivos referenciados no guia estarão sempre utilizando como base o caminho src/….

Em nosso AppModule (app.module.ts), vamos importar o ConfigModule e o TypeOrmModule, deixando o arquivo desta forma:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ItemModule } from './item/item.module';

@Module({
 imports: [
   ConfigModule.forRoot(),
   TypeOrmModule.forRoot({
     type: 'postgres',
     host: process.env.DB_HOST,
     port: parseInt(process.env.DB_PORT),
     username: process.env.DB_USERNAME,
     password: process.env.DB_PASSWORD,
     database: process.env.DB_DATABASE,
     entities: [__dirname + '/**/*.entity{.ts,.js}'],
     synchronize: (process.env.DB_SYNCHRONIZE === 'true'),
   }),
   ItemModule,
 ],
 controllers: [AppController],
 providers: [AppService],
})
export class AppModule { }

 

Importante

A propriedade synchronize do TypeOrmModule ficou habilitada no arquivo .env e é útil na fase de desenvolvimento, pois realiza a sincronização automática do banco de dados com as especificações das entidades da aplicação. Mas não deve ser utilizada em produção, pois pode causar a perda de dados!

 

Extra

Que tal utilizar a env SERVER_PORT no main.ts, ao invés de a deixar fixa na porta 3000? Confira uma dica na documentação do ConfigModule: https://docs.nestjs.com/techniques/configuration#using-in-the-maints

 

Definindo o Item

Agora, podemos finalmente definir nossa entidade Item. Abra o arquivo item/entities/item.entity.ts e atualize o conteúdo para:

import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";

@Entity()
export class Item extends BaseEntity {
 @PrimaryGeneratedColumn('uuid')
 id: string;

 @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
 updatedAt: Date;

 @Column({ name: 'name', type: 'varchar', length: 50 })
 name: string;

 @Column({ name: 'description', type: 'varchar', nullable: true, length: 255 })
 description?: string;

 @Column({ name: 'quantity', type: 'int' })
 quantity: number;
}

Perceba que as definições do mapeamento do objeto relacional do TypeORM é toda realizada por anotações nas classes e atributos.

O mapeamento padrão para o tipo string é uma coluna varchar(255) e para o tipo number é um integer (dependendo do banco de dados). Confira mais informações na documentação.

No exemplo da coluna description que declaramos, poderia ser utilizado apenas @Column({ nullable: true }), mas preenchemos tudo para um maior detalhamento. Para o updatedAt, foi utilizado o decorator @UpdateDateColumn, que atualiza o valor automaticamente, sempre que um update for feito no registro. Existem também os @CreateDateColumn, @DeleteDateColumn e @VersionColumn. O TypeORM os chama de Special Columns.

 

Nossa entidade agora está pronta para uso. 🎉️

Se iniciarmos a aplicação agora, a tabela do banco será criada automaticamente com a estrutura definida acima (por conta da propriedade syncronize: true definida no AppModule).

Mas ainda não atualizamos o item.service.ts para utilizar o TypeORM.

 

Antes disso, vamos criar nossos DTOs?

Definido os Objetos de Transferência de Dados (DTOs)

Talvez você tenha notado que nossos ItemController e ItemService gerados não recebem diretamente o Item para as chamadas de Create() e Update(), e sim os [DTOs] CreateItemDto e UpdateItemDto, que o CRUD Generator criou.

Ao utilizarmos DTOs, podemos não expor diretamente nosso modelo interno (do banco de dados) a quem consome a API, e sim uma representação do dado com os atributos relevantes (ou permitidos) para uso externo. Ter um controle maior sobre os dados e a possibilidade de uma melhor performance (consultando no banco apenas as colunas necessárias) são algumas das vantagens ao utilizar DTOs. Então, vamos defini-los.

Abra o item/dto/create-item.dto.ts e substitua o conteúdo por:

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;
}

 

Ficou com um erro por aí?

Foi porque não instalamos a dependência class-validator, responsável por garantir que os valores recebidos em nosso DTO estejam de acordo com o que esperamos. Também precisamos da class-transformer para a validação ser realizada automaticamente. Instale ambas com:

npm install --save class-validator class-transformer

 

Você pode conferir a lista completa de validadores, assim como mais detalhes sobre eles na documentação.

 

O item/dto/update-item.dto.ts já está pronto, dispensando alterações:

import { PartialType } from '@nestjs/mapped-types';
import { CreateItemDto } from './create-item.dto';

export class UpdateItemDto extends PartialType(CreateItemDto) {}

 

Aqui o extends PartialType(CreateItemDto) faz o trabalho pegando as propriedades do CreateItemDto e as reaproveitando automaticamente; porém deixa todos os atributos como opcionais.

Vamos atualizar o main.ts para realizar a validação automática, adicionando a linha:

app.useGlobalPipes(new ValidationPipe());

 

O arquivo ficará assim:

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(3000);
}
bootstrap();

Você pode ver mais detalhes (e configurações) do validador na documentação.

 

Implementando o ItemService

Como comentamos anteriormente, o item/item.service.ts é chamado pelo controlador (ItemController) e é responsável pela regra de negócio. O código gerado pelo CRUD Generator contém os métodos básicos que precisamos, mas não estão implementados (apenas retornam as mensagens explicativas).

Com o padrão Repositório (Repository), criamos uma abstração para a forma como obtemos os dados para a entidade: se é do banco de dados padrão da aplicação ou de um secundário, API externa via REST ou qualquer outra forma.

Como no caso de Item temos um CRUD simples e estamos utilizando o TypeORM, vamos utilizar o Repository que ele nos fornece, deixando o arquivo assim:

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 utilizando um provider externo (o Repository do TypeORM), precisamos declarar seu módulo como um import do item.module.ts, deixando o arquivo 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 { }

 

Para finalizar, agora o item.controller.ts deve apresentar erros nos três últimos métodos (findOne(), update() e remove()) por conta das chamadas ao itemService que agora esperam um id do tipo string (antes era um número). Substitua os +id por apenas id para remover o cast e…

 

Voilà!

Agora nossa API está implementada e é possível realizar operações de cadastro, leitura, atualização e remoção (CRUD) de itens! 👏🏼️

 

Para realizar todas as operações, você precisa enviar os requests REST via Postman, Insomnia, curl ou da forma que preferir para os seguintes endereços (de acordo com o item.controller.ts):

Documentando com OpenAPI (Swagger)

Antes de finalizarmos, vamos mostrar como é simples documentar a API de acordo com a especificação da OpenAPI(um formato comumente utilizado para descrever APIs REST e facilitar a vida de outros desenvolvedores que estejam integrando à nossa API), utilizando o módulo do Swagger.

Com poucos passos, o Swagger estará funcionando. Comece instalando as dependências:

npm install --save @nestjs/swagger swagger-ui-express

 

Em seguida, atualize o main.ts e o deixe 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('Shopping list API')
   .setDescription('My shopping list API description')
   .setVersion('1.0')
   .build();
 const document = SwaggerModule.createDocument(app, config);
 SwaggerModule.setup('api', app, document);

 await app.listen(3000);
}
bootstrap();

 

Agora, precisamos apenas atualizar nossa entidade e os DTOs com decorators para que o Swagger identifique-os:

No arquivo item/dto/create-item.dto.ts, adicione os imports e @ApiProperty() aos atributos. No caso de description, que é opcional, utilize @ApiPropertyOptional(). Também é possível informar algumas opções, como valor de exemplo e descrição. O arquivo pode 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: 'Cavendish bananas', description: 'Optional description of the item' })
 @IsOptional()
 @IsString()
 description: string;

 @ApiProperty({ example: 5, description: 'Needed quantity' })
 @IsInt()
 @Min(0)
 quantity: number;
}

 

Existem decorators e opções adicionais que você também pode adicionar aos métodos dos controllers e descrever as funções. Confira o código de exemplo do NestJS.

E para o item/dto/update-item.dto.ts, é mais simples:

import { PartialType } from '@nestjs/swagger';
import { CreateItemDto } from './create-item.dto';
 
export class UpdateItemDto extends PartialType(CreateItemDto) {}

 

Está praticamente igual, apenas trocando o import { PartialType } do @nestjs/mapped-types para o @nestjs/swagger.

 

Executando a aplicação

Chegou o grande momento! Agora a aplicação está pronta e com uma “documentação”. Podemos iniciar e ver tudo funcionando com:

npm run start:dev

 

Abrindo no navegador a URL http://localhost:3000/api, será exibida a página do Swagger com a API detalhada, incluindo os DTOs! Pode-se inclusive realizar chamadas de teste por lá mesmo.

Confira uma demonstração de uso:

 

E a segurança?

Na maioria das vezes, queremos que nossa API tenha uma autenticação de usuário para liberar o acesso aos serviços disponíveis. Não abordamos estes aspectos neste guia, mas a documentação do NestJS tem uma seção específica sobre o assunto em https://docs.nestjs.com/security/authentication.

Além do tópico de autenticação, recomendamos as seguintes leituras e ativações em seu servidor:

  • Helmet, que adiciona cabeçalhos HTTP para ajudar a tornar o serviço um pouco mais seguro;
  • CORS, para limitar as requisições vindas de browsers por domínio;
  • CSRF, faz uso do csurf  para evitar ataques do tipo CSRF (falsificação de solicitação entre sites);
  • Rate limiting, para amenizar as tentativas de ataques de força-bruta.

 

Importante

Estas implementações não tornam sua aplicação à prova de ataques, mas ajudam a proteger de algumas vulnerabilidades comuns e/ou básicas.

 

Conclusão

Como falamos no início, por ter uma arquitetura definida e vários plugins prontos para uso, o desenvolvimento de uma API com NestJS, especialmente com o auxílio da NestCLI, é simples e ordenado, com um código organizado, bem estruturado por padrão, seguindo boas práticas e facilmente documentável via Swagger.

Neste guia, mostramos apenas um ponto de partida. Continue a praticar incrementando o código (que tal criar a entidade Categoria para adicionar aos Itens?), escrevendo testes e adicionando uma autenticação para a API.