Venho trabalhando com NestJS em meus projetos pessoais desde 2019, basicamente são projetos para expor pequenas APIs que servem a alguns poucos e pequenos sites.
Recentemente tive a oportunidade de compor um time que estava usando NestJS de uma forma muito séria e fizemos a implementação de uma API muito robusta que esta escalando mais e mais.
Estou muito satisfeito com o resultado que o NestJS tem nos proporcionado e isso me motivou a fazer esses post.
O que é NestJS
Framework de NodeJs que possibilita aos desenvolvedores de TypeScript e JavaScript criar aplicações eficientes e capazes de escalar de maneira muito simples e rápida.
Escrevi TypeScript antes de JavaScript porque o NestJS é TypeScript first ou seja, todo o desenvolvimento é baseado em TypeScript mas é claro você ainda pode usar JavaScript (fique à lá vonté).
Um detalhe sobre o NestJS, "por baixo dos panos" NestJS faz uso do nosso querido Express. Você não é obrigado usar o Express, você pode trocar por Fastify. Esse é um post padrão, então vai seguir com o Express no exemplo!
Vamos ao tutorial
Primeiramente, vamos instalar e fazer uso da Nest CLI que é um command-line que vai nos ajudar a inicializar o projeto, nos ajuda no desenvolvimento e a manter o projeto também.
$ npm i -g @nestjs/cli
$ nest new project-name
Como demonstração neste post vamos criar uma API para registro de contatos.
Execute o comando:
nest new api-contact
Quando aparecer a pergunta - Which package manager would you ❤️ use? - selecione "npm".
Navega até a pasta:
cd api-contact
Você verá a seguinte estrutura inicial do projeto:
O scaffolding do NestJS já traz vários arquivos de projetos que nos ajudam muito durante o desenvolvimento.
Uma coisa, antes de começar a codificar, vamos adicionar o TypeORM
npm i --save @nestjs/typeorm typeorm
Agora que já temos a dependência do TypeORM instalado, vamos criar nossa primeira entidade.
Crie um arquivo chamado contact.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; | |
@Entity() | |
export class Contact { | |
@PrimaryGeneratedColumn() | |
id: number; | |
@Column() | |
fullName: string; | |
@Column() | |
birth: Date; | |
@Column() | |
phoneNumber: number; | |
@Column() | |
email: string; | |
@Column() | |
address: string; | |
} |
Agora vamos fazer uma de uma DTO que será usada para salvar os registros de contatos, então crie o arquivo CreateContact.dto.ts:
export class CreateContactDto { | |
fullName: string; | |
brith: Date; | |
phoneNumber: number; | |
email: string; | |
address: string; | |
} |
Não vou falar hoje sobre padrão Service Repository, mas pra quem conhece esse conceito vai identificar isso muito fácil no NestJS.
Crie o arquivo contacts.service.ts:
import { Injectable, InternalServerErrorException } from '@nestjs/common'; | |
import { InjectRepository } from '@nestjs/typeorm'; | |
import { Repository } from 'typeorm'; | |
import { CreateContactDto } from './CreateContact.dto'; | |
import { Contact } from './contact.entity'; | |
@Injectable() | |
export class ContactsService { | |
constructor( | |
@InjectRepository(Contact) | |
private readonly repository: Repository<Contact>, | |
) {} | |
public async findContact(contactId: string): Promise<Contact> { | |
return this.repository.findOne(contactId); | |
} | |
public async findAll(): Promise<Contact[]> { | |
return this.repository.find({}); | |
} | |
public async CreateContact(input: CreateContactDto) { | |
const savedContact = await this.repository.save({ ...input }); | |
if (!savedContact) { | |
throw new InternalServerErrorException( | |
'Problem ocorried when trying to create a new contact', | |
); | |
} | |
return savedContact; | |
} | |
} |
Obviamente a repository do exemplo salva dados em um banco de dados.
Vou fornecer aqui as configurações que eu usei para o PostgreSQL. Para isso usei o arquivo app.module.ts que é padrão já fornecido pelo scaffolding do NestJS:
import { Module } from '@nestjs/common'; | |
import { TypeOrmModule } from '@nestjs/typeorm'; | |
import { AppController } from './app.controller'; | |
import { AppService } from './app.service'; | |
import { Contact } from './contact.entity'; | |
import { ContactsController } from './contacts.controller'; | |
import { ContactsService } from './contacts.service'; | |
@Module({ | |
imports: [ | |
TypeOrmModule.forRoot({ | |
type: 'postgres', | |
host: '127.0.0.1', | |
port: 5432, | |
username: 'postgres', | |
password: 'example', | |
database: 'contactList', | |
synchronize: true, | |
entities: [Contact], | |
}), | |
TypeOrmModule.forFeature([Contact]), | |
], | |
controllers: [AppController, ContactsController], | |
providers: [AppService, ContactsService], | |
}) | |
export class AppModule {} |
Pra facilitar a vida de quem acompanha esse post vou deixar o arquivo docker-compose.yml disponível para quem tem interesse de já rodar a imagem de um PostgreSQL e testar a API.
version: "3.8" | |
services: | |
postgres: | |
image: postgres:13.1 | |
restart: always | |
environment: | |
POSTGRES_PASSWORD: example | |
ports: | |
- 5432:5432 | |
adminer: | |
image: adminer | |
restart: always | |
ports: | |
- 8080:8080 |
Até aqui eu só mostrei o exemplo do código de produção, ou seja o código que necessitamos para rodar a API de forma funcional.
Agora, vamos ver a parte do código que corresponde aos Testes Unitários.
Por padrão o NestJS já cria um arquivo de teste que vem junto com o scaffolding.
Para esse exemplo vou deixar o código de testes unitários que usei para testar a nossa service, então veja o código do arquivo contacts.service.spec.ts:
import { | |
NotFoundException, | |
InternalServerErrorException, | |
} from '@nestjs/common'; | |
import { getRepositoryToken } from '@nestjs/typeorm'; | |
import { Test, TestingModule } from '@nestjs/testing'; | |
import { ContactsService } from './contacts.service'; | |
import { Contact } from './contact.entity'; | |
import { CreateContactDto } from './CreateContact.dto'; | |
describe('ContactService', () => { | |
let service: ContactsService; | |
const mockRepository = { | |
find: jest.fn(), | |
findOne: jest.fn(), | |
save: jest.fn(), | |
update: jest.fn(), | |
}; | |
beforeAll(async () => { | |
const module: TestingModule = await Test.createTestingModule({ | |
providers: [ | |
ContactsService, | |
{ | |
provide: getRepositoryToken(Contact), | |
useValue: mockRepository, | |
}, | |
], | |
}).compile(); | |
service = module.get<ContactsService>(ContactsService); | |
}); | |
beforeEach(() => { | |
mockRepository.find.mockReset(); | |
mockRepository.findOne.mockReset(); | |
mockRepository.save.mockReset(); | |
mockRepository.update.mockReset(); | |
}); | |
it('should be defined', () => { | |
expect(service).toBeDefined(); | |
}); | |
describe('When search All Contacts', () => { | |
it('should be list all Contacts', async () => { | |
const c1 = new Contact(); | |
c1.id = 10; | |
c1.fullName = 'Contact 1'; | |
const c2 = new Contact(); | |
c2.id = 11; | |
c2.fullName = 'Contact 2'; | |
mockRepository.find.mockReturnValue([c1, c2]); | |
const contactsResult = await service.findAll(); | |
expect(contactsResult).toHaveLength(2); | |
expect(mockRepository.find).toHaveBeenCalledTimes(1); | |
}); | |
}); | |
describe('find a contact', () => { | |
it('find a existing contact', async () => { | |
const contact = new Contact(); | |
contact.id = 10; | |
contact.fullName = 'Contact 1'; | |
mockRepository.findOne.mockReturnValue(contact); | |
const contactResult = await service.findContact('10'); | |
console.log(contactResult); | |
expect(contactResult).toMatchObject({ | |
id: contact.id, | |
fullName: contact.fullName, | |
}); | |
expect(mockRepository.findOne).toHaveBeenCalledTimes(1); | |
}); | |
it('should return an exception when do not find a contact', async () => { | |
mockRepository.findOne.mockReturnValue(null); | |
expect(service.findContact('3')).rejects.toBeInstanceOf( | |
NotFoundException, | |
); | |
expect(mockRepository.findOne).toHaveBeenCalledTimes(1); | |
}); | |
}); | |
describe('Create Contact', () => { | |
it('should create a Contact', async () => { | |
const input = new CreateContactDto(); | |
input.fullName = 'Contact 1'; | |
mockRepository.save.mockReturnValue(input); | |
const savedContact = await service.CreateContact(input); | |
expect(savedContact).toMatchObject(input); | |
expect(mockRepository.save).toBeCalledTimes(1); | |
}); | |
it('should return an exception when do not create a contact', async () => { | |
const input = new CreateContactDto(); | |
input.fullName = 'Contact 1'; | |
mockRepository.save.mockReturnValue(null); | |
await service.CreateContact(input).catch((e) => { | |
expect(e).toBeInstanceOf(InternalServerErrorException); | |
expect(e).toMatchObject({ | |
message: 'Problem ocorried when trying to create a new contact', | |
}); | |
}); | |
expect(mockRepository.save).toBeCalledTimes(1); | |
}); | |
}); | |
}); |
Eu implementei alguns poucos testes, porém o que eu quero demonstrar é a facilidade que temos em montar nossos testes unitários com Jest e NestJS.
Veja como é fluída a codificação dos testes.
Código completo do exemplo no ❤️Github, onde tudo esta!
Conclusão
NestJS é um framework muito estável e de fácil adoção em aplicações de todos os níveis de complexidade isso porque ele é muito simples de se usar e também porque Nest CLI já te serve uma ótima estrutura de código para começo de projeto e de fácil alteração e evolução.
Top comments (3)
Boa!
Grupo BR no Telegram sobre NestJS pra quem quiser tirar dúvidas e discutir sobre o fw: t.me/nestjsbr
I desire to learn nestjs
Look at my github github.com/tarcisiocorte that I always update more and more javascript/typescript code.