Criando interfaces e classes abstratas na camada main da aplicação no CrazyStack Node.js

Dev Doido - Feb 27 '23 - - Dev Community

Nesta aula, vamos explorar o uso de interfaces e classes abstratas na camada principal da aplicação. Vamos ver como essas abstrações podem ser usadas para tornar o código mais flexível e escalável, ao mesmo tempo que melhoram a organização e entendimento.

Este é um material auxiliar do bootcamp CrazyStack Node.js do DevDoido. Ele servirá como uma espécie de documentação de alguns códigos vistos durante as aulas apenas como material complementar, garanta já sua vaga no bootcamp clicando AQUI!.

Veremos como definir interfaces para representar ações da aplicação, e como usar classes abstratas para fornecer uma estrutura comum para vários tipos de objetos relacionados. Veremos como essas abstrações podem ser usadas para melhorar a manutenibilidade do código, ao facilitar a criação de novas funcionalidades e a alteração de funcionalidades existentes sem prejudicar o resto da aplicação.

No final desta aula, você terá uma compreensão mais clara de como interfaces e classes abstratas podem ser usadas na camada principal da aplicação e como essas técnicas podem ser aplicadas em seus próprios projetos.
Este é um material auxiliar do bootcamp CrazyStack Node.js do DevDoido. Ele servirá como uma espécie de documentação de alguns códigos vistos durante as aulas apenas como material complementar, garanta já sua vaga no bootcamp clicando AQUI!.

import { HttpRequest, HttpResponse } from "@/application/helpers";
export interface Middleware {
  handle(httpRequest: HttpRequest): Promise<HttpResponse>;
}
Enter fullscreen mode Exit fullscreen mode

Esta é uma interface "Middleware" que define o método "handle" para receber um objeto "HttpRequest" e retornar uma promessa de objeto "HttpResponse". Ela é usada para fornecer uma estrutura para criar camadas de middleware na aplicação. O objeto "HttpRequest" representa a solicitação HTTP e o objeto "HttpResponse" representa a resposta HTTP.

As classes que implementam essa interface são responsáveis por realizar tarefas específicas, como autenticação, validação de dados, manipulação de erros, entre outras, antes de enviar a solicitação para o próximo middleware ou para o controlador de destino.

export interface Authentication {
  auth(email: string, password: string): Promise<any>;
  authRefreshToken(userId: string): Promise<any>;
}
Enter fullscreen mode Exit fullscreen mode

A interface Authentication define dois métodos: auth e authRefreshToken. O método auth recebe dois parâmetros: email e password, e retorna uma promessa que, quando resolvida, retorna um valor qualquer. Já o método authRefreshToken recebe um único parâmetro userId e também retorna uma promessa.

Esta interface é utilizada para definir a forma como a autenticação deve ser realizada em uma aplicação. Quem implementa esta interface é responsável por definir o comportamento dos métodos para realizar a autenticação de acordo com as regras da aplicação.

export interface Validation {
  validate(input: any): Error[];
}
Enter fullscreen mode Exit fullscreen mode

Esta interface Validation define um método chamado validate que recebe um parâmetro input e retorna um array de erros. A implementação desta interface seria responsável por validar os dados de entrada de acordo com algum critério específico. Por exemplo, se os dados de entrada devem seguir um formato específico ou se algum dos valores é obrigatório, essa validação seria feita na implementação desta interface.

Interfaces e classes abstratas são formas de implementar políticas de projeto e garantir a consistência do código, especialmente quando se trata de separação de responsabilidades e de padrões de projeto.

Neste caso, as interfaces Middleware, Authentication e Validation são usadas para definir a interface para implementações futuras. Isso permite que novos componentes sejam adicionados ao sistema com maior facilidade, pois eles precisam seguir as definições dessas interfaces.

export class UnauthorizedError extends Error {
  constructor() {
    super("Unauthorized");
    this.name = "UnauthorizedError";
  }
}
export class ForbiddenError extends Error {
  constructor(error: any) {
    super("Forbidden");
    this.name = "ForbiddenError";
    this.stack = error?.stack;
  }
}
export class ServerError extends Error {
  constructor(error: any) {
    super("Internal Server Error");
    this.name = "ServerError";
    this.stack = error?.stack;
  }
}
export class MissingParamError extends Error {
  constructor(paramName: string) {
    super(`Missing param: ${paramName}`);
    this.name = "MissingParamError";
  }
}
export class InvalidParamError extends Error {
  constructor(paramName: string) {
    super(`Invalid param: ${paramName}`);
    this.name = "InvalidParamError";
  }
}
export class EmailInUseError extends Error {
  constructor() {
    super(`The received email is already in use`);
    this.name = "EmailInUseError";
  }
}
export class AccessDeniedError extends Error {
  constructor() {
    super("Access denied");
    this.name = "AccessDeniedError";
  }
}
export class AuthenticationError extends Error {
  constructor() {
    super("Authentication failed");
    this.name = "AuthenticationError";
  }
}
Enter fullscreen mode Exit fullscreen mode

As classes de erro (UnauthorizedError, ForbiddenError, etc) são usadas para garantir a consistência do tratamento de erros em todo o aplicativo. Ao invés de simplesmente lançar uma exceção genérica, essas classes personalizadas podem ser usadas para identificar com mais precisão o tipo de erro que ocorreu, tornando mais fácil para os desenvolvedores lidarem com esses erros e corrigi-los.

import { ForbiddenError, UnauthorizedError, ServerError } from "@/application/errors";
export type HttpResponse<T = any> = { statusCode: number; data: T };
export type HttpRequest<T = any> = {
  body?: T;
  headers?: T;
  params?: T;
  query?: T;
  userLogged?: T;
  userId?: string;
};

export const ok = <T = any>(data: T): HttpResponse<T> => ({ statusCode: 200, data });
export const badRequest = <T = any>(error: T): HttpResponse<T> => ({
  statusCode: 400,
  data: error,
});
export const unauthorized = (): HttpResponse<Error> => ({
  statusCode: 401,
  data: new UnauthorizedError(),
});
export const forbidden = (error: any): HttpResponse<Error> => ({
  statusCode: 403,
  data: new ForbiddenError(error),
});
export const serverError = (error: any): HttpResponse<Error> => ({
  statusCode: 500,
  data: new ServerError(error),
});
Enter fullscreen mode Exit fullscreen mode

Essa classe HttpResponse é uma classe helper para trabalhar com respostas HTTP em sua aplicação. É uma boa prática ter uma classe dessas para padronizar as respostas HTTP, tornando o código mais legível e fácil de manter.

A classe HttpResponse define um tipo HttpResponse<T = any> que consiste em um objeto com duas propriedades: statusCode e data. statusCode é o código HTTP da resposta e data é os dados que serão retornados para o cliente.

A classe também define funções ok, badRequest, unauthorized, forbidden e serverError, que retornam uma resposta HTTP com o respectivo status code.

Por exemplo, se você quiser retornar uma resposta 200 (OK) para o cliente com algum dado, você pode usar a função ok da seguinte maneira:

const data = { message: "Hello World" };
const response = ok(data);
console.log(response);
// Output: { statusCode: 200, data: { message: "Hello World" } }` 
Enter fullscreen mode Exit fullscreen mode

Da mesma forma, se você quiser retornar uma resposta 401 (Unauthorized), você pode usar a função unauthorized:

const response = unauthorized();

console.log(response);
// Output: { statusCode: 401, data: UnauthorizedError {} }` 
Enter fullscreen mode Exit fullscreen mode

A classe HttpRequest é semelhante à classe HttpResponse, porém serve para definir o formato da requisição HTTP. Ela define um tipo HttpRequest<T = any> que consiste em um objeto com propriedades para o corpo da requisição, cabeçalhos, parâmetros da rota, query string e informações do usuário logado (se houver).

import { HttpRequest, HttpResponse, serverError } from "@/application/helpers";

export abstract class Controller {
  abstract execute(httpRequest: HttpRequest): Promise<HttpResponse>;
  async handle(httpRequest: HttpRequest): Promise<HttpResponse> {
    try {
      return this.execute(httpRequest);
    } catch (error) {
      return serverError(error);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Este código define a classe abstrata Controller em uma aplicação. Essa classe tem dois métodos: execute e handle.

O método execute é uma função abstrata, ou seja, deve ser implementada pelas classes filhas de Controller. Ele recebe um objeto httpRequest e retorna uma promessa que, quando resolvida, irá retornar um objeto HttpResponse.

O método handle é uma função concreta, ou seja, já está implementada na classe Controller. Ele também recebe um objeto httpRequest, mas antes de chamar o execute, ele faz uma tentativa de capturar qualquer erro que possa ocorrer durante a execução. Caso ocorra algum erro, ele retorna o resultado da função serverError passando o erro como argumento. Se não houver erro, o método execute é chamado normalmente e o resultado é retornado.

Note que as funções HttpRequest, HttpResponse e serverError são importadas do módulo @/application/helpers.

A classe abstrata Controller é extendida por todos os controllers da API de agendamentos online, pois ela fornece uma estrutura básica para todos os controllers seguirem. Ela fornece um método handle que é responsável por manipular a lógica principal dos controllers.

A lógica principal é implementada pelo método execute, que é declarado como abstrato na classe Controller e é obrigatório ser implementado em todas as subclasses. O método execute é responsável por receber uma requisição HTTP e retornar uma resposta HTTP.

Se algum erro ocorrer durante a execução do método execute, ele é capturado pelo bloco try-catch no método handle e a função serverError é chamada para retornar uma resposta HTTP de erro.

Assim, a classe abstrata Controller fornece uma estrutura padronizada e confiável para todos os controllers da API de agendamentos online seguirem e implementarem a lógica principal.

Em comparação, no NestJS, a estruturação dos controllers é feita a partir da utilização de decorators e classes que já possuem métodos implementados. Por exemplo, ao invés de ter que implementar manualmente o método handle, o NestJS fornece uma classe chamada Controller que já possui esse comportamento. Além disso, o NestJS também fornece outras funcionalidades como injeção de dependências e validação de entrada, tornando o desenvolvimento mais ágil e eficiente.

Ambas as abordagens são válidas e cada uma possui suas vantagens e desvantagens. A escolha por uma ou outra dependerá das necessidades e preferências da equipe de desenvolvimento.

Optar por uma classe abstrata para os controllers em vez de utilizar a estrutura pronta de um framework, como o NestJS, pode trazer vários benefícios para o desenvolvimento de uma aplicação. O principal é a independência do framework, ou seja, a possibilidade de mudar de framework sem que isso afete a camada de controle.

Ao utilizar a classe abstrata, o código fica mais flexível e menos preso ao framework, o que é importante porque o framework pode mudar ou ser descontinuado ao longo do tempo. Além disso, a classe abstrata permite uma melhor separação das camadas da aplicação, permitindo que elas possam ser testadas e reutilizadas mais facilmente.

Outra vantagem da classe abstrata é que ela fornece uma estrutura básica para todos os controllers da aplicação, tornando mais fácil para os desenvolvedores implementarem novos controllers de maneira consistente. Além disso, a classe abstrata pode ser estendida para adicionar funcionalidades extras que são comuns a todos os controllers, como o tratamento de erros ou o acesso a dados.

Em resumo, usar uma classe abstrata para os controllers em vez de depender de uma estrutura pronta do framework traz benefícios como a independência do framework, uma melhor separação de camadas e uma estrutura básica consistente para todos os controllers. Ao considerar esses benefícios, é fácil entender porque muitos desenvolvedores optam por essa abordagem em vez de usar uma estrutura pronta.

export const cleanDataObject = (
  forbiddenFields: string[],
  allowedFields: string[],
  bodyObject: any
) => {
  const cleanObject: any = {};
  Object.keys(bodyObject).forEach((key) => {
    if (forbiddenFields.includes(key)) {
      return;
    }
    if (allowedFields.includes(key)) {
      cleanObject[key] = bodyObject[key];
    }
  });
  return cleanObject;
};
Enter fullscreen mode Exit fullscreen mode

Esse código é uma função que remove determinados campos de um objeto de requisição HTTP (bodyObject). Ela cria um novo objeto vazio (cleanObject), e verifica cada propriedade do objeto original. Se a propriedade estiver na lista de campos proibidos (forbiddenFields), ele não adiciona ao novo objeto. Se a propriedade estiver na lista de campos permitidos (allowedFields), ele adiciona ao novo objeto. Por fim, retorna o objeto limpo.

Essa função pode ser útil para garantir que apenas os campos permitidos sejam usados para criar ou atualizar recursos em sua aplicação. Ao invés de permitir que o objeto de requisição tenha todos os campos, você pode especificar apenas aqueles que são realmente necessários e relevantes para a sua aplicação, aumentando a segurança e evitando possíveis erros.

LINK DO REPOSITÓRIO

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .