DEV Community

Zoranildo Santos
Zoranildo Santos

Posted on

API Node Desacoplada: Criando Interfaces para Requisições e Respostas HTTP

Como mencionado no artigo anterior, nosso controller(CreateSignUpController.ts) está totalmente acoplado. Antes de criar os adaptadores para as rotas vamos alterar nosso controller pra que fique desacoplado e independente de qualquer ferramenta esterna.

Padronizando as respostas e requisições em uma API Node desacoplada

Vamos definir interfaces para as respostas e requisições de nossa api.

  1. Crie uma pasta chamada shared dentro da pasta infra.
  2. Dentro da pasta shared crie a pasta protocols.
  3. Dentro da pasta protocols crie o arquivo http.ts.

Agora no arquivo http.ts insira o código abaixo:

export type HttpResponse = {
  statusCode: number
  body: unknown
}

export interface HttpRequest {
  body?: unknown
}
Enter fullscreen mode Exit fullscreen mode

HttpResponse:

A interface HttpResponse define a estrutura padrão da resposta que a API enviará para o client após processar uma requisição. Ela possui dois campos:

statusCode: É um campo do tipo number que representa o código de status HTTP que será enviado como resposta ao client(EX: 200, 201, 400, 500).

body: É um campo do tipo unknown, o que significa que a resposta pode conter qualquer tipo de dado. Esse campo armazenará o corpo da resposta enviada ao client, que pode ser, por exemplo, um objeto JSON, uma mensagem de sucesso ou erro, ou até mesmo null, caso a resposta não precise conter um corpo.

Utilizando essa interface, a API pode padronizar o formato das respostas enviadas, o que facilita o tratamento das informações pelo client que está consumindo a API.

HttpRequest:

A interface HttpRequest define a estrutura padrão das requisições que a API receberá do client. Ela possui apenas um campo e é opcional:

body: É um campo do tipo unknown, que representa o corpo da requisição. Esse campo pode conter qualquer tipo de dado, e é geralmente utilizado para passar dados ao servidor, como parâmetros de busca, dados de formulários, ou payloads em requisições POST/PUT.

Ao definir essa interface, a API estabelece um formato padrão para as requisições que ela espera receber, mas permite que o client envie dados de forma flexível, já que o campo body é opcional.

Isso permite que a API se adapte a diferentes tipos de requisições sem impor restrições rígidas. Essas duas interfaces, HttpResponse e HttpRequest, ajudam a promover a separação de responsabilidades e o desacoplamento entre as camadas da aplicação.

Criando um contrato para nossos controllers

  1. Dentro da pasta protocols crie o arquivo controller.ts
  2. No arquivo controller.ts insira o seguinte código:
import { HttpResponse } from './http'

export interface IController<T = unknown> {
  handle(request: T): Promise<HttpResponse>

  // O método handle possui a seguinte assinatura:
  // handle(request:T): Promise<HttpResponse>.

  // Ele recebe um parâmetro chamado request do tipo genérico T,
  // que representa o objeto de requisição que será passado para o 
  // controlador. O método retorna uma Promise que resolve o 
  // HttpResponse
}
Enter fullscreen mode Exit fullscreen mode

Essa interface IController serve como um contrato que deve ser implementado por qualquer classe que atue como um controlador na aplicação. Ao utilizar essa interface, você pode definir vários controladores para diferentes rotas ou recursos da API, mas garantindo que todos eles sigam o mesmo padrão de implementação e retornem uma resposta do tipo HttpResponse.

A vantagem dessa abordagem é a padronização e a flexibilidade que ela proporciona. Essa abstração ajuda a manter o código organizado, facilita a criação de novos controladores e torna a API mais flexível, permitindo a fácil substituição ou adição de controladores sem modificar a lógica central da aplicação.

Criando um contrato para o CREATE do CRUD

  1. Dentro da pasta shared crie o arquivo httpHelper.ts
  2. No arquivo httpHelper.ts insira o seguinte código:
import { HttpResponse } from '../protocols/http'

export const create = (data: unknown): HttpResponse => ({
  statusCode: 201,
  body: data
})
Enter fullscreen mode Exit fullscreen mode

Uma função create é exportada e recebe um parâmetro data do tipo unknown. O tipo unknown indica que a função pode receber qualquer tipo de dado.

A função retorna um objeto do tipo HttpResponse, conforme definido pela interface importada na linha acima.

O objeto retornado pela função create possui dois campos:

statusCode: Representa o código de status HTTP a ser enviado como resposta ao cliente. Neste caso, o valor 201 indica que a requisição foi bem-sucedida e resultou na criação de um novo recurso (usualmente utilizado após uma operação de criação, como um POST).

body: O campo body é definido com o valor do parâmetro data recebido pela função. Isso significa que o conteúdo do campo body será igual ao valor do parâmetro data passado para a função create. Esse campo armazenará o corpo da resposta enviada ao cliente, que pode ser qualquer tipo de dado, incluindo objetos JSON, strings, arrays ou até mesmo null.

A função create é útil para padronizar a criação de respostas no formato HttpResponse, tornando mais fácil e claro o envio de respostas para o client. Outros contratos podem ser criados para update, read e delete com o statusCode 200.

Refatorando o controller(CreateSignUpController.ts)

Código antes da refatoração:

Perceba que o controller está acoplado ao express. Imagine que a aplicação tem dezenas de controllers, se por acaso for necessário mudar o framework, teriamos muito trabalho pra fazer alterando essa tipagem manualmente em todos os controllers.

Com a refatoração, mesmo se a aplicação tiver dezenas de controllers a alteração a ser feita será apenas na camada de infraestrutura, claro, isso se todos os controllers tiverem sido criados seguindo o contrato definido.

import { Response, Request } from "express"
import { CreateSignUpUseCase } from './CreateSignUpUseCase'

interface ICreateSignUpDTO {
  name: string
  password: string
}

export class CreateSignUpController {
  constructor(private readonly useCase: CreateSignUpUseCase) {}

  async handle(req: Request, res: Response): Promise<Response> {
    const { name, password } = req.body as ICreateSignUpDTO

    const data = { name, password }
    await this.useCase.execute(data)

    return res.status(201).send({ message: 'User created successfully' })
  }
}
Enter fullscreen mode Exit fullscreen mode

Código refatorado:

import { CreateSignUpUseCase } from './CreateSignUpUseCase'
import { IController } from '../../../../../infra/shared/protocols/controller'
import { HttpRequest, HttpResponse } from '../../../../../infra/shared/protocols/http'
import { create } from '../../../../../infra/shared/protocols/httpHelper'

interface ICreateSignUpDTO {
  name: string
  password: string
}

export class CreateSignUpController implements IController {
  constructor(private readonly useCase: CreateSignUpUseCase) {}

  async handle(request: HttpRequest): Promise<HttpResponse> {
    const { name, password } = request.body as ICreateSignUpDTO

    const data = { name, password }
    const useCase = await this.useCase.execute(data)

    return create(useCase)
  }
}
Enter fullscreen mode Exit fullscreen mode

Esse foi um belo trabalho. Mesmo assim ainda não é possível alternar entre express e fastify, ainda precisamos criar os adaptadores, o que será feito no próximo artigo.

Top comments (0)