NestJS — Entenda os motivos que levaram a adoção da Brainny e dê os primeiros passos

Róger Javiel Rezende
Brainny Smart Solutions
12 min readJan 21, 2020
Primeiros passos do nestjs com a brainny

Quando nos deparamos com possíveis necessidades de melhoria de código, aqui na Brainny, decidimos utilizar o framework para o Back-End conhecido como NestJS. Neste artigo iremos descrever os motivos que nos levaram a adotar este framework e criar uma aplicação de CRUD de usuários, apresentando os passos iniciais, e conforme a nossa stack adotada, mostraremos as integrações com GraphQl, TypeGraphQL, TypeORM e PostgreSQL.

Nossos Motivos

O NestJS é um framework que auxilia na criação de aplicações eficientes e escaláveis do lado do servidor. Com o NestJS temos uma melhor organização de código, rapidez no desenvolvimento e desacoplamento. Trabalhar com prazos definidos e com mais pessoas é o dia a dia da grande maioria das empresas de desenvolvimento de sistemas e projetos de um modo geral, e como utilizávamos somente o minimalista framework Express, a consequência desta liberdade era ter uma grande tendência à desorganização e falta de padrões nos códigos. Além disso, o NestJS nos possibilita a trabalhar com TypeScript, onde temos as facilidades do auto-complete auxiliando em uma previsão de possíveis erros futuros e uma maior velocidade no desenvolvimento da aplicação (Leia mais sobre isso no nosso Post sobre os 6 motivos para utilizar o TypeScript). A modularização obtida com NestJS também foi decisiva para nossa adoção do framework. Mesmo não sendo obrigatória, temos a possibilidade de separar cada parte importante do nosso sistema em módulos distintos e reutilizarmos quando necessário. Com isso obtemos um desacoplamento significativo e menor escrita desnecessária de código.

Iniciando com NestJS

Para começar um projeto com NestJS é fácil, pois o framework possui uma boa documentação e alguns exemplos claros. O framework possui um CLI que cria os arquivos e estrutura o projeto automaticamente.

  • Versões que estou utilizando neste momento: Node — 12.8.1, NestJS — 6.11.5, TypeGraphQL — 0.17.5 e TypeORM — 0.2.21.

Utilizando o gerenciador de pacotes do Node — npm (ou yarn) — rodamos os seguintes comandos no terminal, onde criaremos a aplicação com o nome de project-nest:

$ npm i -g @nestjs/cli 
$ nest new project-nest
Estrutura de pastas do projeto

O projeto já iniciará estruturado, pré configurado e com uma pasta principal nomeada src, onde ficarão os arquivos do nosso projeto. Podemos destacar que dentro desta pasta temos o módulo principal da aplicação ( app.module.ts) e o arquivo principal ( main.ts). O restante dos arquivos desta pasta nós apagaremos, não serão utilizados neste artigo, pois iremos criar um novo módulo que terá o seu controller e o seu service, entre outros arquivos, no qual faremos a comunicação com este módulo principal. Vale destacar que o NestJS já possui integração com o Jest e alguns arquivos para implementar os testes da aplicação. A imagem ao lado mostra a estrutura de pastas do projeto.

O arquivo principal(main.ts)criará uma instância da aplicação e disponibilizará a porta 3000 para rodar o projeto, conforme imagem abaixo. Esta porta pode ser alterada sem qualquer problema, desde que já não esteja sendo utilizada.

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

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

O módulo principal(module.ts) deverá ficar desta forma após apagar o restante dos arquivos como comentado anteriormente.

import { Module } from '@nestjs/common';

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

Integrando o PostgreSQL e o TypeORM

Neste passo configuraremos o banco de dados (costumeiramente chamado de BD). Utilizaremos como ORM (Object-Relational-Mapper) o TypeORM e como SGBD (Sistema de Gerenciamento de Banco de Dados)o PostgreSQL. Para instalar as três dependências necessárias roda-se o comando abaixo.

$ npm i @nestjs/typeorm typeorm pg

Agora precisamos criar o BD. Eu utilizo o Postico como interface gráfica dos meus bancos, tabelas e registros no Mac, porém existem outras opções gratuitas que podem ser usadas em quaisquer sistemas operacionais, como o pgAdmin. Escolha um nome e crie o banco de dados, como mostrado na imagem abaixo.

Criação do banco de dados

Precisamos configurar o TypeORM. Para isso, neste projeto, utilizaremos as configurações do BD no módulo principal para fins didáticos, porém esta não é uma boa prática, já que as credenciais devem ficar indisponíveis no repositório git (Sempre utilize um repositório git, se ainda não começou, comece!). Futuramente criaremos um artigo de como configurar o arquivo .env no NestJS. Abaixo temos a configuração default. Siga o que está configurado na sua máquina e reflita no código.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'javielrezende',
password: 'ABCDEFGHIJKLM',
database: 'nest-article',
entities: ['dist/**/*.entity{.ts,.js}'],
synchronize: true,
}),

],
controllers: [],
providers: [],
})
export class AppModule {}

Observações sobre três parâmetros:

  • database: Utilize o mesmo nome do banco que foi criado anteriormente;
  • entities: É utilizado para registrar as entidades nas opções de conexão;
  • synchronize: Responsável por criar as tabelas automaticamente quando rodar o servidor, tenha um certo cuidado com este parâmetro caso a aplicação vá subir para produção.

Integrando o GraphQL e TypeGraphQL

Neste momento precisamos integrar o GraphQL na aplicação, e para isso é necessário instalar o apollo-server-express, o graphql-tools, o graphql e o type-graphql.

$ npm i @nestjs/graphql apollo-server-express graphql-tools graphql type-graphql

Feito isso, agora temos que adicionar a configuração do GraphQL no módulo principal, onde através do parâmetro autoSchemaFile conseguimos criar automaticamente o arquivo schema do GraphQL na aplicação.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'javielrezende',
password: 'ABCDEFGHIJKLM',
database: 'nest-article',
entities: ['dist/**/*.entity{.ts,.js}'],
synchronize: true,
}),
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
}),

],
controllers: [],
providers: [],
})
export class AppModule {}

Neste momento chegou a hora de criarmos um novo módulo para os usuários. Quando cria-se um módulo pelo CLI do NestJS, é criada uma pasta, caso não exista, que conterá tudo relacionado a este módulo. Desta forma fica fácil a manutenção do código, já que tudo relacionado a usuários estará apenas dentro desta pasta. Com o uso do CLI , facilmente criamos o módulo, o resolver (que é semelhante ao controller quando usa-se o conceito REST) e o service que conterá possíveis lógicas de negócio.

$ nest g module user
$ nest g service user
$ nest g resolver user

Note que a pasta foi criada e o módulo user já foi inserido dentro dela, além do resolver e do service criado com auxílio do CLI. Também é criado automaticamente os arquivos de teste (.spec). Como não realizaremos testes neste artigo, apagaremos estes arquivos.

Diretório dos usuários

Olhe que incrível é este framework e as facilidades que ele proporciona. Quando criamos o módulo user automaticamente este módulo foi inserido na importação do módulo principal.

Módulo principal com User adicionado automaticamente

Outro detalhe incrível é que quando criamos o service e o resolver, eles também já foram adicionados no módulo, mas desta vez no módulo do user, pois será ali que eles serão providos.

Resolver e service do usuário adicionado automaticamente no próprio módulo.

Iniciaremos este módulo criando um arquivo user.entity.ts dentro da pasta user. Como consta na extensão do arquivo, será nossa entidade que refletirá em uma tabela no banco de dados. Neste e em outros arquivos também usaremos uma biblioteca muito importante, o TypeGraphQL. Utilizando seus decorators, ela auxilia criando os tipos GraphQL automaticamente. Observações sobre os decorators logo após o código:

import { ObjectType, ID, Field } from 'type-graphql';
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@ObjectType()
@Entity()
export class User {
@Field(type => ID)
@PrimaryGeneratedColumn()
id: string;

@Field()
@Column()
name: string;

@Field({ nullable: true })
@Column({ nullable: true, unique: true })
email?: string;
}
  • @ObjectType(): Importado do pacote type-graphql, ele é responsável por marcar a classe como um tipo conhecido no GraphQL.
  • @Entity(): Importado do pacote typeorm, é responsável por mapear a classe, refletindo em uma tabela de banco de dados.
  • @Field(): Importado do pacote type-graphql, ele é responsável por marcar o campo e indicar o seu tipo para o GraphQL.
  • @Column(): Importado do pacote typeorm, indica que o campo será uma coluna no banco de dados.

Agora precisamos criar um arquivo com os campos necessários para o cadastro de usuários, dentro de uma pasta chamada DTO (Data Transfer Object — É um padrão de projetos bastante utilizado para o transporte de dados entre cliente e servidor. Em resumo, escolheremos quais campos o cliente enviará para a aplicação back-end, onde no nosso caso, enviaremos apenas o nome e telefone, já que o id será gerado automaticamente.). Também precisamos criar um arquivo que será utilizado para estabelecer os campos de input utilizados no update de um registro. A única diferença deste arquivo para o create é que todos os campos serão permitidos que sejam nulos.

DTO de usuários

Dentro do arquivo faremos também as validações necessárias para cada campo com o uso do pacote chamado class-validator. Após o código explicarei sobre os decorators que foram importados deste e de demais pacotes, quando informado:

import { InputType, Field } from 'type-graphql';
import {
IsString,
MinLength,
MaxLength,
IsNotEmpty,
IsEmail,
IsOptional,
} from 'class-validator';

@InputType()
export class CreateUserInput {
@Field()
@IsString()
@MinLength(3)
@MaxLength(60)
@IsNotEmpty()
name: string;

@IsOptional()
@Field({ nullable: true })
@IsEmail()
email?: string;
}
  • @InputType(): Importado do pacote type-graphql, declara que os campos serão utilizados como entrada de dados para o GraphQl.
  • @IsString(): Verifica se o valor recebido é uma string, caso contrário lança uma exception na requisição.
  • @MinLength(): Verifica se o valor recebido possui menos que 3 caracteres, caso isso aconteça é lançado uma exception na requisição.
  • @MaxLength(): Verifica se o valor recebido possui mais que 60 caracteres, caso isso aconteça é lançado uma exception na requisição.
  • @IsNotEmpty(): Verifica se o valor recebido é diferente de empty (!==’ ‘), null (!== null), ou undefined (!== undefined). Se o valor for verdadeiro será lançado uma exception na requisição.
  • @IsOptional(): Este decorator faz com que somente seja aplicada a validação caso esse campo seja enviado na requisição, caso contrário a validação será desconsiderada.
  • @IsEmail(): Verifica se o valor recebido está nos padrões de um e-mail. Caso contrário será lançado uma exception na requisição.

Para a validação funcionar corretamente temos que instalar o pacote class-transformer com o comando abaixo:

npm i class-transformer

Agora basta adicionar a configuração necessária no arquivo principal da aplicação:

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

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

Gosto sempre de separar o módulo em 3 camadas distintas. O repository que será responsável pela persistência do banco de dados, o service que se comunicará com o repository e será também o responsável pela lógica envolvendo a regra de negócio da aplicação e por último o resolver, que é a camada responsável pela interpretação dos dados de entrada e saída da aplicação. Iniciaremos pelo repository, onde estarão todos os métodos do CRUD. Para isso necessitamos criar um arquivo chamado user.repository.ts dentro da pasta user. Esta classe estenderá a classe Repository passando a entidade do nosso módulo e obtendo todos os métodos utilizados para persistir no BD.

Utilizamos o decorator @EntityRepository(), passando a entidade User. Este decorator declara que estaremos criando um repository personalizado para esta entidade, onde estarão os métodos para persistência no BD.

import { Repository, EntityRepository } from 'typeorm';
import { User } from './user.entity';
import { CreateUserInput } from './dto/create-user.type';
import { UpdateUserInput } from './dto/update-user.type';

@EntityRepository(User)
export class UserRepository extends Repository<User> {
async createAndSave(createUserInput: CreateUserInput): Promise<User> {
const user = await this.save(this.create(createUserInput));
return await this.findById(user.id);
}

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

async findById(id: string): Promise<User> {
return await this.findOne(id);
}

async findAndUpdate(dbUser: User, data: UpdateUserInput): Promise<User> {
await this.update(dbUser.id, { ...data });
const updatedUser = this.create({ ...dbUser, ...data });
return updatedUser;
}

async deleteById(id: string): Promise<boolean> {
await this.delete(id);
return true;
}
}

Agora temos que definir qual o repositório é utilizado no escopo atual, no nosso caso o UserRepository, podendo assim ser injetado no UserService, utilizando o decorator @InjectRepository(). Declaramos isto importando no user.module.ts, como a imagem abaixo.

import { Module } from '@nestjs/common';
import { UserResolver } from './user.resolver';
import { UserService } from './user.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserRepository } from './user.repository';


@Module({
imports: [TypeOrmModule.forFeature([UserRepository])],
providers: [UserResolver, UserService],
})
export class UserModule {}

Agora precisamos criar a camada service. Essa classe é referente a um dos conceitos fundamentais do NestJS, os providers, onde somente inclui-se o decorator @Injectable(). Assim acaba tornando-se possível de ser injetável como parâmetro em construtores, mantendo um relacionamento entre instâncias onde for necessário.

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
import { CreateUserInput } from './dto/create-user.type';
import { UpdateUserInput } from './dto/update-user.type';

@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: UserRepository,
) {}

async createAndSave(createUserInput: CreateUserInput): Promise<User> {
return this.userRepository.createAndSave(createUserInput);
}

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

async findById(id: string): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new NotFoundException('Usuário não encontrado');
}
return user;
}

async findAndUpdate(id: string, data?: UpdateUserInput): Promise<User> {
const dbUser = await this.findById(id);
return this.userRepository.findAndUpdate(dbUser, data);
}

async delete(id: string): Promise<boolean> {
const dbUser = await this.findById(id);
const deleted = await this.userRepository.deleteById(dbUser.id);
if (deleted) {
return true;
}
return false;
}
}

Note que nesta classe estamos verificando a existência do retorno dos dados solicitados ao repository quando necessário, lançando exceptions quando algum erro ocorre. Se for necessário uma lógica maior, nesta classe que será escrita, mantendo uma organização no código. Foi utilizado também um decorator chamado @InjectRepository(), responsável por permitir que seja injetado um repository custom neste service, criado anteriormente.

Agora precisamos criar o resolver de usuários. Vale ressaltar que injetaremos a classe UserService comentada acima, para ser utilizada no resolver. Os decorators que comentarei, após a imagem, são todos importados do pacote @nestjs/graphql.

import { Resolver, Mutation, Args, Query } from '@nestjs/graphql';
import { UserService } from './user.service';
import { User } from './user.entity';
import { CreateUserInput } from './dto/create-user.type';
import { UpdateUserInput } from './dto/update-user.type';

@Resolver('User')
export class UserResolver {
constructor(private userService: UserService) {}

@Mutation(returns => User)
async createUser(@Args('data') data: CreateUserInput): Promise<User> {
return this.userService.createAndSave(data);
}

@Query(returns => [User])
async allUsers(): Promise<User[]> {
return this.userService.findAll();
}

@Query(returns => User)
async user(@Args('id') id: string): Promise<User> {
return this.userService.findById(id);
}

@Mutation(returns => User)
async updateUser(
@Args('id') id: string,
@Args('data') data?: UpdateUserInput,
): Promise<User> {
return this.userService.findAndUpdate(id, data);
}

@Mutation(returns => Boolean)
async deleteUser(@Args('id') id: string): Promise<boolean> {
return this.userService.delete(id);
}
}
  • @Resolver: Indica que esta classe será utilizada como um resolver, já explicado acima.
  • @Mutation: Indica que será do tipo Mutation, no qual será requisitado que algum dado entre cliente e servidor seja manipulado.
  • @Query: Indica que será do tipo Query, no qual será requisitado que retorne algum dado do servidor.
  • @Args: Utilizado para passar os argumentos para as requisições.

Pronto! Agora precisamos rodar o comando abaixo. Assim subiremos o servidor e já poderemos testar todas as queries e mutations.

$ npm run start:dev

O NestJS conta com o Playground do Apollo já integrado, desta forma poderemos fazer todas as requisições sem mesmo ter qualquer ferramenta instalada. Para utilizá-lo somente abrimos o navegador e inserimos o endereço que está logo abaixo. Outras opções que poderia indicar seria o Insomnia e o Postman, muito utilizadas aqui na Brainny.

  • Criando dois usuários:
Criando um usuário
Criando um usuário
  • Retornando a listagem de usuários cadastrados:
Retornando usuários cadastrados
  • Alterando o nome do primeiro usuário cadastrado:
Update em um usuário
  • Retornando um usuário pelo id:
Retornando um usuário pelo id
  • Deletando o usuário de id 2:
Apagando um usuário
  • Retornando novamente todos os usuários cadastrados, ratificando que as mutations de update e delete foram eficientes.
Retornando todos os usuários cadastrados
  • Mostrando sobre o funcionamento das validações, farei uma tentativa frustrada de cadastrar um usuário, primeiramente com menos de 3 caracteres, que é o mínimo que definimos no DTO de cadastro de um cliente, e por último, tentando colocar o e-mail fora do padrão.
Validando os campos de entrada de dados
Validando os campos de entrada de dados

Desta forma podemos ter uma idéia da grandiosidade deste framework e o quanto ele pode facilitar no desenvolvimento de grandes aplicações. Por isso ele foi escolhido para ser utilizado aqui na Brainny. Com sua utilização podemos facilmente ter aplicações escaláveis, testáveis e disponíveis, o que trás uma maior confiabilidade para os nossos clientes.

Este código está disponível no meu GitHub. Acesse: https://github.com/javielrezende/nest-article

--

--