API Node Desacoplada: Criando Interfaces para Requisições e Respostas HTTP
Zoranildo Santos
Posted on August 1, 2023
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.
- Crie uma pasta chamada
shared
dentro da pastainfra
. - Dentro da pasta
shared
crie a pastaprotocols
. - Dentro da pasta
protocols
crie o arquivohttp.ts
.
Agora no arquivo http.ts
insira o código abaixo:
export type HttpResponse = {
statusCode: number
body: unknown
}
export interface HttpRequest {
body?: unknown
}
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
- Dentro da pasta
protocols
crie o arquivocontroller.ts
- 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
}
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
- Dentro da pasta
shared
crie o arquivohttpHelper.ts
- No arquivo
httpHelper.ts
insira o seguinte código:
import { HttpResponse } from '../protocols/http'
export const create = (data: unknown): HttpResponse => ({
statusCode: 201,
body: data
})
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' })
}
}
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)
}
}
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.
Posted on August 1, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 1, 2023