Construindo uma API com NestJS, PostgreSQL e Docker — Parte 1: Criando nosso primeiro endpoint
Recentemente comecei a estudar Node pensei em desenvolver uma API com funcionalidades básicas, que praticamente todo projeto precisa, como forma de me familiarizar com a linguagem. Para me auxiliar nessa tarefa utilizei o framework NestJS. Nesta série de tutoriais vou compartilhar o que aprendi.
Série completa:
Irei atualizando essa lista com links para as devidas partes conforme eu for postando!
- Parte 1: Criando nosso primeiro endpoint
- Parte 2: Adicionando Autenticação e Autorização
- Parte 3: Finalizando nosso CRUD de usuários
- Parte 4: Adicionando logs à nossa aplicação
- Parte 5: Enviando emails de confirmação e recuperação de senha
- Parte 6: Escrevendo testes
- Parte 7: Deploy!
TL;DR
O código dessa primeira etapa se encontra em um repositório no meu GitHub:
O que é o NestJS?
NestJS é um framework Node com total suporte a TypeScript e que roda sobre frameworks HTTP como expressJS ou Fastify. Ele utiliza diversos elementos de Programação Orientada a Objetos e uma série de funcionalidades do TypeScript (como decorators) para facilitar ao máximo nossa tarefa como desenvolvedores.
Ele ainda oferece uma arquitetura de projetos baseada em módulos que se assemelha muito à arquitetura utilizada pelo Angular.
Os requisitos de nossa API
A ideia é que ao final dessa série tenhamos um projeto com as seguintes funcionalidades:
- Criação de conta para usuários externos
- Criação de conta para usuários Administradores
- Autenticação e Autorização
- Envio de emails para confirmação de cadastro
- Envio de emails para recuperação de senha
- Busca de usuários com filtros e paginação
- Busca de dados de um usuário específico
- Bloquear o acesso de um usuário
- Manter um log do que acontece no servidor
Nossa API utilizará o PostgreSQL como banco de dados e usaremos o Docker para facilitar nossa vida de desenvolvedores. Nessa primeira parte do tutorial vamos tratar da configuração do ambiente e criação do nosso primeiro endpoint, “Criação de conta para usuários Administradores”.
Instalação do NestJS
A primeira coisa que você precisa ter instalada na sua máquina é, claro, o Node.js (e junto com ele, o npm). Para isso basta seguir as instruções no site.
Com o Node e o npm instalados, vamos começar a instalar a CLI (Command Line Interface ) do NestJS. Para isso, basta executar o seguinte comando no terminal:
$ npm i -g @nestjs/cli
Caso tenha algum problema durante a instalação, certifique-se de que tem as permissões necessárias, ou utilize o sudo se estiver no Linux e for necessário. Com o pacote da linha de comando instalado, podemos utilizá-lo para criar nosso projeto:
$ nest new nestjs-pgsql-api
Após a execução deste comando, basta selecionar o npm como gerenciador de dependências. Caso prefira poderá utiliar o yarn, entretanto durante o restante deste tutorial utilizaremos o npm. Quando o comando finalizar de gerar a base de seu projeto, podemos entrar dentro do diretório:
$ cd nestjs-pgsql-api
Abrindo o projeto, você se depara com essa estrutura de pastas:
Podemos observar entre os arquivos gerados a pasta src, onde ficará nosso código e a pasta test para arquivos de teste. Você pode já rodar o seguinte comando para ter certeza de que tudo está ok:
$ npm run start:dev
Se tudo estiver ok você pode acessar http://localhost:3000/ no seu navegador e a mensagem “Hello World!” será exibida.
Configuração do PostgreSQL com o Docker
Utilizaremos o Docker em conjunto com o Docker Compose para configuração do nosso ambiente. Para manter o foco deste tutorial, não abordarei como instalar e configurar estas ferramentas. Instruções podem ser encontradas nas documentações oficiais ou em diversos tutoriais na internet. Caso não deseje utilizar o Docker para configuração do ambiente pode instalar o PostgreSQL em sua máquina e configurar um usuário e senha para acesso. O importante é você ter acesso a um banco de dados Postgre.
Vamos adicionar um arquivo docker-compose.yml na raiz de nosso projeto com as configurações de nossos containers.
Você irá notar que também adicionei um container rodando o adminer. Adminer é um sistema de gerenciamento de banco de dados (SGBD) escrito em PHP, bem leve e com todas as funcionalidades que precisaremos neste tutorial.
Para testar se está tudo ok, vamos subir nossos containers:
$ docker-compose up -d
O comando pode demorar um pouco para executar caso ainda não tenha as imagens na sua máquina. Quando ele finalizar, você poderá acessar o adminer em http://localhost:8080/. Selecione o sistema PostgreSQL, informe o servidor, que no caso será o nome que demos ao nosso container: pgsql. Informe o nome de usuário e senha, pguser e pgpassword respectivamente. Caso tenha substituído estes parâmetros em seu arquivo docker-compose.yml certifique-se de substituí-los aqui também. Clique em Entrar.
Se tudo estiver correto você será redirecionado para a página de gerenciamento de banco de dados. Nela, clique em “Criar Base de dados” e crie o banco de dados nestjs, que usaremos no decorrer deste tutorial.
Conectando com o Banco de Dados
Precisamos agora conectar nossa aplicação NestJS com o banco de dados que acabamos de criar. Para tal utilizaremos o TypeORM. Vamos então instalar os pacotes necessários:
$ npm i --save typeorm @nestjs/typeorm pg
Agora, antes de começarmos a escrever nosso código, vamos fazer uma pequena limpeza no nosso projeto, removendo alguns arquivos que não serão utilizados.
- Podemos apagar a pasta test por completo
- Remover app.controller.spec.ts em src
- Remover app.controller.ts em src
- Remover app.service.ts em src
Lembre-se também de remover os respectivos imports no arquivo app.module.ts:
Sua estrutura de pastas deve ter ficado dessa forma:
Agora dentro da pasta src vamos criar uma pasta configs e dentro dela vamos criar o arquivo typeorm.config.ts:
Vamos adicionar agora o módulo do TypeORM aos imports globais, no arquivo app.module.ts.
Após isso, rode novamente o comando para executar o projeto:
$ npm run start:dev
Se tudo estiver ok o projeto deve inicializar normalmente. Caso ocorra algum erro verifique se seguiu corretamente os passos até o momento.
Criando nosso primeiro endpoint
Nosso primeiro endpoint será referente à criação de um usuário com privilégios de administrador. Para tal, primeiro vamos criar nosso módulo de usuários, o módulo do nosso sistema responsável por tratar todas as questões que envolvam os usuários. Para adicionar um novo módulo ao projeto, basta executar o seguinte comando:
$ nest g module users
O que este comando faz? Ele cria uma pasta no diretório src com o nome do módulo escolhido e dentro dela cria um arquivo de declaração do módulo, nesse caso com o nome user.module.ts, e já adiciona a dependência no arquivo app.module.ts.
Dentro da nova pasta users vamos criar o arquivo user.entity.ts. Este arquivo terá a declaração da estrutura do nosso Usuário e será com base nele que o TypeORM irá gerar a tabela no banco de dados.
Observe os campos createdAt e updatedAt. Esses campos serão preenchidos automaticamente pelo TypeORM com timestamp de quando o objeto foi criado/alterado.
Podemos agora executar o comando:
$ npm run start:dev
E quando o projeto terminar de compilar, podemos acessar o painel do Adminer e dentro do nosso banco de dados nestjs veremos que foi criada a tabela user com a estrutura que descrevemos no users.entity.ts.
Junto com nosso arquivo user.entity.ts podemos já criar também nosso arquivo users.repository.ts, que irá conter a declaração do nosso repositório para acesso ao banco de dados. Um repositório e a camada de nossa aplicação responsável por realizar a persistência com nosso banco de dados. De novo, o TypeORM simplifica bastante as coisas:
Agora devemos adicionar nosso repositório aos imports do nosso módulo users. O NestJS utiliza Injeção de Dependências para realizar o controle de dependências entre diferentes módulos e também mantê-los o mais desacoplado possível. No arquivo users.module.ts, que foi gerado automaticamente para nós através da CLI, vamos adicionar nosso repositório aos imports como um módulo do TypeORM:
Agora podemos criar nossa camada de serviço do módulo, responsável por tratar da lógica básica por trás da execução de nossos endpoints. Novamente, a CLI pode nos ajudar nessa tarefa:
$ nest g service users --no-spec
O parâmetro no-spec serve apenas para que não seja criado um arquivo de testes junto com nosso Service, uma vez que não trataremos de testes ainda. Além de gerar o arquivo users.service.ts automaticamente para nós, o comando ainda adiciona no nosso arquivo users.module.ts a devida dependência.
É interessante observar o uso do Decorator @Injectable que é o responsável por fazer com que nossa classe faça parte do sistema de Injeção de Dependências do NestJS. Vamos já agora fazer uso novamente da Injeção de Dependências e “injetar” nosso repositório no serviço criado, já que o serviço irá utilizá-lo para comunicar-se com o banco de dados:
Vamos adicinar no users.service.ts o método createAdminUser que será responsável por tratar da criação de nosso usuário administrador. Para isso o método receberá como parâmetros o email, o nome, a senha e a confirmação de senha do usuário. Entretanto, nossos requisitos podem (e muito provavelmete vão) mudar no futuro, por isso é bom utilizarmos um DTO (Data Transfer Object).
Um DTO é um objeto que será composto pelos dados que serão utilizados entre diferentes métodos da aplicação com um objetivo em comum. Dessa forma, vamos criar então nosso create-user.dto.ts em uma nova pasta dtos dentro de nosso módulo users.
Os demais campos de nosso usuário serão preenchidos no decorrer da aplicação. Como o processo de criação de um usuário pode ficar bastante extenso, podemos mover a lógica de criação para nosso repositório, mantendo assim nosso users.service.ts limpo e conciso. Além do mais, nosso método de criação de usuários poderá ser reaproveitado no futuro quando estivermos implementando a funcionalidade de Sign-Up (cadastro de um usuário comum, sem privilégios de administrador). Já que estamos discutindo sobre diferentes funções, vamos criar um Enum com as nossas roles (no momento apenas duas, mas outras podem ser adicionadas no futuro). Ele pode ser criado dentro da pasta users mesmo.
Vamos adicionar ao nosso users.repository.ts a lógica para criação de um usuário. Mas antes disso vamos adicionar o pacote bcrypt que será responsável por codificar nossas senhas, seguindo boas práticas de segurança.
$ npm i --save bcrypt
Após as modificações nosso users.repository.ts ficará da seguinte forma:
Observe que:
- Já criamos o campo confirmationToken que será utilizado no futuro para confirmação do email do usuário
- Recebemos como parâmetro no método a função do usuário na forma de nosso RolesEnum, fazendo com que ela possa posteriormente ser reutilizado na criação de um usuário comum
- Fizemos um tratamento especial para o código de erro 23505, que segundo a documentação do PostgreSQL é o código de erro retornado quando há conflito de um campo marcado como único —
unique_violation
- Geramos um salt para nosso usuário, também seguindo boas práticas de segurança (pode ler sobre aqui)
Agora podemos chamar esse método criado no repositório em nosso serviço para criação de nosso usuário:
Por último, precisamos criar a rota que irá desencadear esse processo. Rotas são declaradas em Controllers no NestJS. Vamos usar a CLI para criar nosso controller:
$ nest g controller users --no-spec
Esse será o arquivo gerado:
O parâmentro ‘users’ passado para o Decorator @Controller serve para indicar que esse controller irá tratar das requisições feitas para a URI http://localhost:3000/users. Vamos adicionar ao nosso controller o endpoint responsável pela criação de um usuário admin e que retornará o usuário criado, bem como uma mensagem de sucesso. Podemos novamente definir um DTO com essas carcterísticas já que as mesmas podem ser reaproveitadas por outros métodos no futuro:
No nosso users.controller.ts podemos agora adicionar o método de criação de usuário administrador:
Repare que utilizamos o Decorator @Post como forma de identificar o método HTTP que deverá ser utilizado para acessar a rota. Podemos passar como parâmetro para esse Decorator o caminho da URI que ele irá tratar. Como nenhum caminho foi especificado fica subentendido que ele irá tratar o caminho “/”, ou seja, http://localhost:3000/users/.
Vamos agora testar nosso endpoint criado utilizando o Postman.
Vamos enviar a seguinte requisição:
Ela deve gerar uma resposta semelhante a essa:
E se acessarmos o Adminer também podemos conferir que nosso usuário foi salvo no banco de dados.
Agora, nenhuma validação está sendo feita na nossa API para garantir que todos os dados necessários para a criação de um usuário estão sendo recebidos. O que acontece se recebermos uma requisição diferente do esperado? Receberemos um erro 500 com uma mensagem “Erro ao salvar o usuário no banco de dados”, conforme especificamos no nosso repositório.
Como podemos validar nossa entrada de dados para retornarmos uma mensagem de erro mais específica?
A resposta é simples, podemos utilizar o pacote class-validator:
$ npm i --save class-validator class-transformer
Ele adiciona uma série de Decorators que podemos utilizar no nosso DTO para especificarmos validações específicas para cada campo, bem como a mensagem de erro que deve ser retornada para cada caso:
Você também pode usar o Decorator @Matches para verificar se a senha informada corresponde a uma expressão regular específica, caso deseje melhorar a segurança de sua aplicação.
Mas para que essas validações entrem em ação devemos fazer uma pequena alteração no nosso controller, passando o parâmetro ValidationPipe para nosso decorator @Body que realiza a extração de dados da requisição:
Agora podemos realizar um teste com algumas informações incorretas:
Que deve nos retornar uma resposta com código 400 Bad Request, muito mais apropriado ao tipo de erro, bem como um relatório detalhado de todos erros encontrados na requisição:
E com isso concluímos a primeira parte do nosso tutorial. Na próxima parte vamos tratar de Autenticação e Autorização. Tem alguma dúvida ou sugestão? Escreva nos comentários abaixo!