Recentemente lancei a primeira parte de uma série de tutoriais sobre NestJS aqui no blog, que você confere neste link. Hoje, vamos dar sequência aos aprendizados da primeira lição, então parto do pressuposto que você já tem um projeto NestJS criado na sua máquina e que quando roda ele no navegador, uma rota GET é acionada e com isso a mensagem Hello World é visível. Procede?
Então vamos em frente!
#1 – Ciclo de Vida de Request
A primeira coisa que você precisa entender é o ciclo de vida de uma request no NestJS. Para isso, primeiro você precisa entender como o HTTP em si funciona, algo que ensino neste outro artigo. Entendido como o HTTP funciona, a request chega no seu backend NestJS através do módulo principal, definido no arquivo main.ts. Se preferir você pode assistir ao vídeo abaixo para entender o ciclo de vida.
Se olharmos novamente o nosso main.ts, veremos o seguinte:
1 2 3 4 5 6 7 8 9 10 |
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap(); |
Repare que o AppModule é citado na inicialização do nosso servidor, o que quer dizer que ele é o módulo principal da nossa aplicação. Toda request será enviada para ele por padrão. Cabe ao módulo decidir o que fazer com esta requisição e essa decisão vai ser baseada na URI da request e nos controllers que estiverem à disposição do módulo.
Vamos olhar novamente o app.module.ts para ver o que temos nele.
1 2 3 4 5 6 7 8 9 10 11 12 |
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [], controllers: [AppController], providers: [AppService], }) export class AppModule {} |
Repare na propriedade controllers do decorator @Module que temos apenas um controller, o AppController, mas como é um array, poderiam ser vários. Toda request vai ser confrontada com a rota base definida dentro dos controllers importados. Se olharmos um controller no detalhe, perceberemos melhor isso, como o app.controller.ts abaixo (ou quase, mas já explico).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { return this.appService.getHello(); } } |
O decorator @Controller define que esta classe é um controller, ou seja, recebe requests HTTP. Mas ela define também qual a rota base deste controller, sendo que o padrão é “/”, ou seja, a raiz do backend. Experimente por exemplo mudar para @Controller(“teste”) e reinicie o backend para ver que no navegador terá de digitar http://localhost:3000/teste para ver o Hello World agora.
Então do AppModule vai para o AppController. Mas como o AppController sabe qual função deve ser chamada? Novamente, pela URL, mas também pelo verbo ou método HTTP. De acordo com o método utilizado, sendo que via browser o padrão é GET, ele vai procurar quais funções do controller atendem tal método. Dentre as funções, ele vai verificar quais atendem à URL.
Como assim?
Olhe especificamente para a função getHello novamente.
1 2 3 4 5 6 |
@Get() getHello(): string { return this.appService.getHello(); } |
Repare o decorator @Get() no topo dela. Isso indica que esta função é chamada por requests GET. Além disso, define que não é necessária nenhum trecho de URL adicional à base, sendo chamada na raiz do endpoint mesmo. Ou seja, você pode combinar uma URL base do controller com mais um trecho de URL na função para definir o direcionamento mais adequado às suas requests.
Como exemplo, mude o decorator @Controller para @Controller(“users”) e o decorator @Get da função getHello para @Get(“luiz”). Isso vai fazer com que você tenha de passar a URL http://localhost:3000/users/luiz no navegador para ver o Hello World.
E existe ainda um último ponto no ciclo de vida de requests do NestJS antes de devolvermos uma resposta para o usuário, que são os services. Enquanto que os controllers fazem o papel de rotear as requisições, cabe aos services a tarefa de processá-las de fato. Entenda como processamento qualquer atividade de negócio da sua aplicação, como cálculos, persistência, etc.
Se olhar o código anterior novamente, verá que ele chama o appService, que nada mais é do que uma instância da classe definida em app.service.ts.
1 2 3 4 5 6 7 8 9 10 |
import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { getHello(): string { return 'Hello World!'; } } |
E é aqui que você encontra de fato o Hello World escrito. Experimente mudar para ver o resultado nos seus testes de backend. Essa arquitetura é resumida na imagem abaixo.
Um último ponto importante de ser citado aqui é que o AppService tem o decorator @Injectable para que ele possa ser injetado no controller, algo que acontece no constructor dele, como mostra a linha abaixo.
1 2 3 4 5 6 7 |
import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} |
Isso é necessário devido à arquitetura do NestJS, baseada em Injeção de Dependências, algo que podemos conversar sobre no futuro mas que para o momento não é necessário entender em profundidade.
Dito isso, vamos exercitar nossos conhecimentos atuais de NestJS criando um primeiro backend com CRUD completo.
#2 – Salvando Dados
Vamos começar pelo C do CRUD, de Create. Como ainda não vimos nada sobre persistência de dados em NestJS, algo que merece um tutorial à parte, nós vamos fazer este CRUD todo em memória. Mais tarde, você pode facilmente plugar um banco de dados no service, substituindo a lógica que vamos deixar. Não se preocupe, o mais importante no momento é aprendermos como rotear e processar as requisições e não a persistência em si.
Lembra-se do ciclo de vida que acabamos de estudar? É nele que vamos nos basear para desenvolver cada uma das etapas necessárias. main.ts e app.module.ts não precisam de alterações, começaremos a partir do app.service.ts, criando uma função addUser, um type de User e uma variável local de users. Adivinha que tipo de dado nosso CRUD vai salvar?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { Injectable } from '@nestjs/common'; export type User = { id: number; name: string; age: number; uf: string; } @Injectable() export class AppService { users: User[] = []; addUser(newUser: User): User { const nextId = this.users.length > 0 ? this.users[this.users.length - 1].id + 1 : 1; newUser.id = nextId; this.users.push(newUser); return newUser; } } |
Vamos por partes, primeiro, nosso type de User. Esse type vai definir os dados que todo User vai ter, como se fosse um schema do banco de dados. Se você não conhece TypeScript, tem de aprender o básico dele para conseguir usar NestJS na sua plenitude. O mesmo vale para classes, que você tem de entender como funcionam em JavaScript, caso contrário terá problemas aqui. Repare que exportei o type, pois vamos precisar dele mais tarde em outros locais.
Depois, definimos no corpo da classe um array de User. Essa variável será responsável por guardar em memória todos nossos users, para esses testes sem banco de dados.
E por fim, temos a função addUser, que receberá um objeto user, gerará um id auto-incremental para ele (usando o último id gerado como base) e salvará o novo user no array em memória, retornando o último cadastrado no retorno da função. Repare que esta função não faz qualquer menção a requests HTTP, é uma função normal que mais tarde você pode adaptar para salvar no banco de dados, por exemplo.
Mas como eu uso esta função em uma request?
Vamos voltar no app.controller.ts e vamos incluir no decorator do controller o endpoint “users”, pois este vai ser um controller de gestão dos users da aplicação a partir de agora. Ou seja, para chamar nossa API, deverá usar a URL http://localhost:3000/users a partir de agora, combinando com qualquer outra URL que tiver nas rotas deste controller.
1 2 3 4 5 6 7 |
import { Controller, Get, Post, Patch, Put, Delete, Body, Param } from '@nestjs/common'; import { AppService, User } from './app.service'; @Controller("users") export class AppController { |
Repare também que importei várias coisas do @nestjs/common que vamos usar ao longo deste tutorial e também o type de User lá do app.service.ts.
Agora vamos criar uma função addUser no app.controller.ts também, que diferente da addUser do service, essa é responsável pelo roteamento da requisição.
1 2 3 4 5 6 |
@Post() addUser(@Body() user: User): User { return this.appService.addUser(user); } |
Toda função de controller precisa de um decorator informando o método HTTP que ela espera, opcionalmente com alguma nova parte de URL a ser concatenada com a URL base do controller. Como não definimos nenhuma URL, então quer dizer que basta fazer um POST na URL base do controller (/users) para que esta rota seja chamada.
Na sequência, temos outro decorator, mas desta vez antes do parâmetro user. Decorators antes de parâmetros de uma função do controller indicam de onde esse parâmetro será parseado. Neste caso, o decorator @Body indica que o objeto user virá no corpo da request e usei do type User que definimos anteriormente para ajudar na manipulação deste dado.
Já o corpo da função em si é bem direto: pega o user que veio no body e envia pro service processar, devolvendo o retorno dele como retorno do controller também.
Para testar, basta você salvar todos seus arquivos e colocar o backend a rodar novamente. Como é uma rota POST, você deve usar o Postman ou outra ferramenta equivalente (como Insomnia). Abaixo um print de como pode fazer a request, não esquecendo de incluir o header Content-Type como application/json, que não aparece na imagem.
Caso nunca tenha usado Postman antes, o vídeo abaixo pode ajudar.
Vamos em frente!
#3 – Lendo dados
Agora que aprendemos como fazer uma rota de POST, fazer outras rotas acaba se tornando bem simples, principalmente as rotas de leitura. O padrão REST para APIs determina que devemos ter ao menos duas rotas de leitura, uma que liste todos os registros (ou paginado) e outra que lista apenas um registro.
Volte ao app.service.ts e adicione mais duas funções.
1 2 3 4 5 6 7 8 9 |
getUsers(): User[] { return this.users; } getUser(id: number): User { return this.users.find(u => u.id == id); } |
A primeira apenas retorna o array inteiro, dispensa explicações adicionais. A segunda, usa um find para encontrar o user com o id informado no parâmetro. O find é uma High Order Function, falo mais delas no vídeo abaixo, caso não conheça as principais, é um recurso muito poderoso do JavaScript.
Agora com estas funções em mãos, vamos voltar ao app.controller.ts para criar primeiro a rota que lista todos users, como abaixo.
1 2 3 4 5 6 |
@Get() getUsers(): User[] { return this.appService.getUsers(); } |
Nada digno de nota, certo? É um GET na raiz do controller (definido no decorator acima da função), que não espera parâmetro algum e devolve o array de users recebido do service.
E por fim, a função que retorna apenas um usuário.
1 2 3 4 5 6 |
@Get(":id") getUser(@Param("id") id: number): User { return this.appService.getUser(id); } |
Aqui temos alguns detalhes e o primeiro deles é o “:id” no decorator de Get. Isso indica que esse GET vai ser tratado somente se tiver um parâmetro id na URL, após a base do controller. Ex: http://localhost:3000/users/x onde x é um id qualquer.
Para pegar este id, precisamos usar o decorator @Param antes do parâmetro da função, indicando o nome do parâmetro que queremos capturar (que deve ser o mesmo do @Get). Isso vai fazer com que aquele trecho da URL onde deveria estar o id seja capturado e colocado na variável id. Essa variável id vai ser usada pelo service para fazer o filtro que programamos anteriormente.
Para testar, você pode usar o Postman novamente, apenas ajustando sua request de acordo. No exemplo abaixo, trazendo apenas um usuário com id 2. Se eu tirar o /2 do final, vai listar todos.
Apenas atenção que a cada execução, como está tudo salvo em memória, você vai ter de cadastrar do zero pois se perde.
#4 – Atualizando dados
Agora vamos fazer as funções de atualização de usuários. Sim, no plural, pois são duas previstas no padrão REST: quando queremos atualizar toda uma entidade usamos o verbo PUT e quando queremos atualizar apenas alguns campos, o verbo PATCH.
Vamos começar sempre pelo service, fazendo as duas funções.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
replaceUser(id: number, newData: User): User { const index = this.users.findIndex(u => u.id == id); if (index === -1) throw new NotFoundException(); newData.id = id; this.users[index] = newData; return newData; } updateUser(id: number, newData: User): User { const index = this.users.findIndex(u => u.id == id); if (index === -1) throw new NotFoundException(); const user = this.users[index]; if (newData.name) user.name = newData.name; if (newData.age) user.age = newData.age; if (newData.uf) user.uf = newData.uf; this.users[index] = user; return user; } |
Na função replaceUser, recebemos o id que vamos usar como filtro e os dados do novo user que usaremos como substituto. Aí com a findIndex achamos a posição em memória do usuário a ser substituído e jogamos por cima dele os novos dados, se foi encontrado. Se o findIndex não encontrar nada, disparamos uma exceção que deve ser importada da @nestjs/common, chamada NotFoundException. Essa exceção vai fazer com que a resposta a essa requisição seja um 404 Not Found, que é o padrão para estas situações.
Já na função updateUser, é um pouco mais complexo. Ao encontrarmos um usuário com o id informado, nós testamos os campos que foram passados em newData, a fim de substituir um a um, somente o que for necessário, retornando o usuário atualizado ao final.
Agora devemos ir para o app.controller.ts para criar estas duas rotas.
1 2 3 4 5 6 7 8 9 10 11 |
@Put(":id") replaceUser(@Param("id") id: number, @Body() newData: User): User { return this.appService.replaceUser(id, newData); } @Patch(":id") updateUser(@Param("id") id: number, @Body() newData: User): User { return this.appService.updateUser(id, newData); } |
Depois de fazer as rotas anteriores não temos tantas novidades por aqui, apenas uma combinação de conceitos que já vimos. Tanto o decorator @Put quanto @Patch define que um parâmetro :id será passado na URL e que o mesmo será usado como filtro das funções (@Param). Ambas funções também definem que os dados a serem usados na atualização virá no corpo da requisição (@Body). Internamente, apenas passam esses parâmetros capturados para as respectivas funções do service, que fará o processamento adequado (atualização ou substituição).
O teste via Postman não é muito diferente do que já vimos antes, apenas atente-se ao uso correto do verbo e definição da URL e corpo que não tem erro.
#5 – Exclusão de Dados
E por fim, vamos fazer o D do CRUD, de Delete. Comece no app.service.ts definindo a função de exclusão em memória.
1 2 3 4 5 6 7 8 9 |
deleteUser(id: number): boolean { const index = this.users.findIndex(u => u.id == id); if (index === -1) throw new NotFoundException(); this.users.splice(index, 1); return true; } |
Para isso, procuramos pelo usuário baseado em seu id e depois usamos splice para eliminar esta posição do array sem deixar espaços vazios. Em caso de sucesso retornaremos um true, caso contrário, a exceção de não encontrado (que vira código 404 na response HTTP).
Agora vamos ao app.controller.ts fazer esta última função que é até mais simples que as anteriores.
1 2 3 4 5 6 |
@Delete(":id") deleteUser(@Param("id") id: number): boolean { return this.appService.deleteUser(id); } |
Atenção apenas ao decorator de @Delete antes da função, que espera um parâmetro id na URL que será usado como filtro internamente (@Param). No mais, foi apenas chamar a função do service para que esteja tudo funcionando.
E com isso finalizamos um CRUD completo seguindo as regras do padrão REST. Claro, manipulamos apenas dados em memória, mas esta era a base fundamental de NestJS que você precisa ter para fazer as suas APIs. Em tutoriais futuros, vamos aprender a fazer a mesma coisa, mas usando banco de dados em conjunto.
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.