Alura > Cursos de Programação > Cursos de Node.JS > Conteúdos de Node.JS > Primeiras aulas do curso Padrões de projeto com TypeScript: aprimorando uma API com arquitetura limpa

Padrões de projeto com TypeScript: aprimorando uma API com arquitetura limpa

Desacoplando a regra de negócio - Apresentação

Boas-vindas ao curso de Design Patterns ou Padrão de Projetos!

Sou Emerson Laranja, instrutor na Escola de Programação da Alura.

Audiodescrição: Emerson é um homem negro com barba e cabelos escuros. Usa óculos de grau quadrado e uma camiseta verde. Ao fundo, uma parede lisa com iluminação degradê do verde ao azul.

Se você tem experiência com TypeScript criando APIs e busca ir além do código funcional, aplicando boas práticas e padrões de arquitetura conhecidos no mercado, esse curso é para você!

O que você aprenderá:

Para isso, usaremos uma API para adicionar e remover tarefas na nossa To Do List. É algo similar a uma tarefa do Google, onde temos uma lista. Podemos adicionar uma tarefa e também excluí-la.

Prepararemos a API para respeitar essas duas funcionalidades de adicionar e remover.

Pré-requisitos

Para você aproveitar melhor esse conteúdo, é importante que já tenha feito o curso de Solid com TypeScript.

Conheça todos os recursos da plataforma

Além de assistir os vídeos, te convidamos a aproveitar os outros recursos da plataforma como as atividades, comunidade do Discord para encontrar outras pessoas que estejam estudando o mesmo que você e o Fórum para tirar dúvidas.

Te esperamos no vídeo seguinte. Até lá!

Desacoplando a regra de negócio - Criando padrão de arquitetura

Nessa etapa do projeto, queremos criar uma lista de tarefas.

Para isso, seguiremos uma estrutura semelhante ao diagrama abaixo.

Diagrama de fluxo de adição de tarefa com três componentes interconectados: 'AddTaskController' no topo com linhas direcionadas para três losangos rotulados 'Validator', 'MongoDB' e 'Express' dispostos horizontalmente abaixo.

Temos um controller, o AddTask, e quando queremos lidar com as rotas desse controller, instalamos a biblioteca do Express e criamos uma dependência desse controller com o Express.

Geralmente, fazemos o mesmo com o nosso banco de dados. Se queremos fazer manipulações, adicionamos o cliente MongoDB, por exemplo, e para validar nossos dados, instalamos uma biblioteca, como o validator, e tudo isso fica ligado ao nosso controller.

Mas qual é o problema disso? Queremos continuar seguindo as boas práticas, assim como do Solid para codificação. Porém, agora aprenderemos boas práticas relacionadas à arquitetura.

A questão é que o que temos agora não pode ser chamada de arquitetura. Temos um componente que está fazendo muitas coisas, com várias responsabilidades.

Descobriremos a solução para separar as responsabilidades e as camadas que pertencem ao nosso domínio, como, por exemplo, o AddTaskController, e o que é uma biblioteca externa, como o Validator, MngoDB e Express.

O resultado dessa arquitetura será um diagrama composto por três blocos. Porém, inicialmente focaremos no bloco do adapter, onde fazemos a conexão com o controller.

Diagrama de fluxo de controle e adaptação de software com cinco componentes principais interconectados, conforme descrição abaixo no texto.

Temos novamente o AddTaskController e o Express, porém, na ligação entre eles foram criados mais dois itens. Uma interface Controller, indicada em pontilhado, e um componente que servirá para adaptar o que o Express precisa para as necessidades do controller. Descobriremos na prática como fazer essa separação.

Separando as responsabilidades

No VS Code e abrimos o projeto. Acessamos src > adapters > interfaces > controller.ts. Esse é um dos arquivos que já deixamos prontos. Nesse caso o controller é quem implementa o método handle(), que recebe um HTTPRequest, do tipo HTTPRequest, que já deixamos implementado, e retornaremos uma promise a HTTPResponse.

controller.ts

import { HttpRequest, HttpResponse } from "./http";

export interface Controller {
    handle(httpRequest: HttpRequest): Promise<HttpResponse>;
}

Copiamos a linha de código referente ao handle() e fechamos o arquivo. Em sequência, acessamos adapters > controllers > task > addTask.ts.

addTask.ts

Nesse arquivo temos a classe AddTaskController que não está implementando o contrato do controller. Então, próximo à linha 11, após a classe, passamos implements Controller.

A ferramenta indica um erro, pois espera o formato da assinatura. Então, próximo à linha 13, substituímos o trecho de código após async pelo que copiamos anteriormente.

//Código omitido

export class AddTaskController implements Controller{
async handle(httpRequest: HttpRequest): Promise<HttpResponse> {
    const requiredFields = ["title", "description", "date"];
}

//Código omitido

Feito isso, precisamos retornar uma Promise<HttpResponse>. Analisaremos o que precisamos alterar no código. O cont requiredFields, está definindo quais são os campos obrigatórios para criar uma tarefa, que é o title, description e date.

Após, fazemos uma verificação caso algum desses campos seja enviado, caso não retornamos um badRequest() para a pessoa usuária. Caso todos os campos tenham sido recebidos, o selecionamos, fazemos uma validação para ver se a data é válida com a nossa biblioteca validator.

Se for uma data inválida, retornamos novamente badRequest(). Caso contrário, mandamos um JSON falando que aquele objeto foi criado. O created() também é um helper que foi criado para facilitar nossa resposta.

O que precisamos mudar agora é que não temos mais req e sim httpRequest. Então, pressionamos "Ctrl + D" para selecionar onde temos o req e substituímos por httpRequest.

//Código omitido

for (const field of requiredFields) { I
    if (!httpRequest body [field]) {
        return badRequest(new MissingParamError(field));
}

//Código omitido

Agora precisamos retornar o objeto que seja do tipo httpResponse. Como mencionamos, o created() que criamos como um helper é do tipo httpResponse. Então, apagamos o res.json e passamos return created(task).

//Código omitido

const task = { title, description, date }
return created(task);

//Código omitido

Salvamos e no terminal, rodamos o projeto passando o comando npm start.

npm start

Feito isso, recebemos um erro que está acontecendo em taskRoutes.ts. Isso aconteceu, pois estamos usando o Express para lidar com as rotas. Porém, agora o nosso método handle() do controller, espera um httpRequest, ou seja, respeita o que foi definido internamente.

Então, precisamos criar uma nova camada que receberá os dados que precisamos internamente e retornar algo que o Express consegue entender.

Faremos isso na sequência. Até lá!

Desacoplando a regra de negócio - Adaptando as rotas

Estamos tentando desacoplar a biblioteca Express com o controller, que adicionará uma tarefa.

Quando tentamos fazer isso, tivemos um problema no arquivo taskRoutes.ts, pois ele espera um argumento, mas está recebendo dois. Isso acontece, pois o método handle() espera um httpRequest, como definimos internamente. Porém, estamos usando nas nossas rotas o Express, que espera essa nomenclatura com o rep e res.

É como se tivéssemos um notebook novo e tentássemos conectar em um monitor antigo. Geralmente monitores antigos possuem a saída VGA, enquanto o novo apenas HDMI. Dessa forma, não conseguimos fazer o encaixe, exceto se tivermos um adaptador do tipo VGA para HDMI, para que esses dois módulos consigam se comunicar.

O mesmo acontece nesse caso. Para o controller conseguir se comunicar com o Express, precisaremos criar um adaptador de rotas no Express.

Criando o adaptador de rotas no Express

Para isso, no VS Code, acessamos o Explorador na lateral esquerda da tela. Acessamos "src > adapters", na raiz dessa pasta, criaremos um arquivo. Então, acima, clicamos em "Novo arquivo", indicado pelo ícone de uma folha de papel dobrada na ponta. Nomeamos de expressRouteAdapter.ts.

expressRouteAdapter.ts

Começamos passando o export const expressRouteAdapter que receberá, entre parênteses, controller: Controller. Abrimos função e dentro retornaremos algo que o Express entende, nesse caso a função return async(req:Request, res:Response)=>{}.

import { Request, Response } from "express";
import { Controller } from "./interfaces/controller";

export const expressRouteAdapter = (controller: Controller) => {
    return async (req: Request, res: Response) => {
    }

Nas chaves, precisamos passar para o controller um objeto do tipo httpRequest e capturar o retorno, que será um httpResponse. Então, passamos controller.handle(httpRequest).

Mas quem que é esse httpRequest? Se no explorador acessarmos a pasta "interfaces" e abrirmos o arquivo http.ts, notamos que o httpRequest é alguém que possui um objeto do tipo body. Então, fechamos o arquivo e voltamos para o expressRouteAdapter.ts.

Sabendo disso, na linha acima de handle(), criamos o const httpRequest={}, nas chaves passamos body:req.body. Depois, na linha abaixo, antes de controller.handle(), passamos const httpResponse = await.

//Código omitido

export const expressRouteAdapter = (controller: Controller) => {
    return async (req: Request, res: Response) => {
        const httpRequest = {
            body: req.body,
        };
        const httpResponse = await controller.handle(httpRequest);

Agora, precisamos fazer uma verificação. Se o status de tarefa foi criada, ou seja, o 201, retornaremos um status e o body. Caso contrário, teremos que modificar o body para ser um erro.

Na linha abaixo, passamos if(httpResponse.statusCode=201){}. Nas chaves, passamos res.status(httpResponse.statusCode).json(). A mensagem JSON que retornaremos é o body, então, nos parênteses passamos httpResponse.body.

Caso isso não seja verdade, então else{} retornaremos um res.status(httpResponde.statusCode).json() passando no JSON o {error:httpResponse.body.message}, entre chaves, pois estamos retornando um objeto de erro.

import { Request, Response } from "express";
import { Controller } from "./interfaces/controller";

export const expressRouteAdapter = (controller: Controller) => {
    return async (req: Request, res: Response) => {
        const httpRequest = {
            body: req.body,
        };
        const httpResponse = await controller.handle(httpRequest);
        
        if (httpResponse.statusCode = 201) {
            res.status(httpResponse.statusCode).json(httpResponse.body);
        } else {
            res
        .status(httpResponse.statusCode)
        .json({ error: httpResponse.body.message });
        }

O adaptador está pronto. Agora, precisamos chamá-lo no arquivo taskRoutes.ts. Ao acessá-lo, podemos apagar todo o trecho de código após "/tasks". No lugar, adicionamos vírgula e passamos o adaptador expressRouteAdapter(addTaskController) e salvamos o arquivo.

taskRoutes.ts

import { Request, Response, Router } from "express";
import { AddTaskController } from "../../../controllers/task/addTask";
import { express RouteAdapter } from "../../../express RouteAdapter";

export default (router: Router): void => {
    const addTaskController = new AddTaskController();
    router.post("/tasks", expressRouteAdapter(addTaskController));
};

Feito isso, nosso servidor já está executando, caso o seu não esteja basta passar o comando npm start no terminal. Agora, podemos testar.

Com o Insomnia aberto, faremos nossas requisições. Temos uma requisição do tipo POST, apontando para http://localhost:3000/api/tasks que é onde criaremos nossas rotas. Não se preocupe, pois o projeto já está configurado para apontar para essa rota.

Basta passarmos um title, description e um date e enviar.

{
    "title":"Title example",
    "description": "one single description here!",
    "date": "29/02/2024"
}

Depois, na lateral superior direita, clicamos no botão "Send" e temos o objeto sendo retornado logo abaixo e o status 201 criado.

Se analisarmos novamente o primeiro bloco do diagrama, percebemos que concluímos a parte da direita do controller. Separamos o que antes o controller estava ligado ao Express e acoplamos a biblioteca a um adaptador para podermos desacoplar o controller. Agora o controller depende de uma interface que o implemente, não dependemos mais de uma biblioteca externa.

Se futuramente precisarmos trocar de biblioteca, como trocar o Express pelo Fastify, basta alterarmos apenas o adaptador.

Da mesma forma que conseguimos fazer com o Express, precisaremos fazer com a nossa biblioteca de validação. É isso que faremos na sequência.

Te esperamos lá!

Sobre o curso Padrões de projeto com TypeScript: aprimorando uma API com arquitetura limpa

O curso Padrões de projeto com TypeScript: aprimorando uma API com arquitetura limpa possui 114 minutos de vídeos, em um total de 49 atividades. Gostou? Conheça nossos outros cursos de Node.JS em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Node.JS acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas