Skip to content

Instantly share code, notes, and snippets.

@MoisesTedeschi
Created July 23, 2026 12:08
Show Gist options
  • Select an option

  • Save MoisesTedeschi/caa199939c93117fe3b6f62a8650ee0b to your computer and use it in GitHub Desktop.

Select an option

Save MoisesTedeschi/caa199939c93117fe3b6f62a8650ee0b to your computer and use it in GitHub Desktop.
Validação de Oferta
/**
* ============================================================================
* Exemplo sênior: consumo de API com autenticação usando Clean Code
* e padrões de projeto (Strategy, Result Object, Dependency Injection,
* Facade e Custom Errors), mantendo Fetch API como mecanismo de requisição.
*
* Fluxo de negócio modelado:
* 1) Validar oferta (API de validação)
* 2) Se válida, postar order (API de criação de pedido)
* ============================================================================
*/
/* ----------------------------------------------------------------------- */
/* 1. Erros customizados — deixam o motivo da falha explícito e tipado */
/* ----------------------------------------------------------------------- */
class ApiError extends Error {
constructor(message, { status = null, cause = null } = {}) {
super(message);
this.name = "ApiError";
this.status = status;
this.cause = cause;
}
}
class NetworkError extends ApiError {
constructor(cause) {
super("Falha de rede ao chamar a API", { cause });
this.name = "NetworkError";
}
}
class UnauthorizedError extends ApiError {
constructor(status) {
super("Falha de autenticação com a API", { status });
this.name = "UnauthorizedError";
}
}
/* ----------------------------------------------------------------------- */
/* 2. Result — evita usar exceptions para controlar fluxo de negócio. */
/* O chamador decide o que fazer com sucesso/erro sem try/catch */
/* espalhado pelo código. */
/* ----------------------------------------------------------------------- */
class Result {
constructor(isSuccess, value, error) {
this.isSuccess = isSuccess;
this.value = value;
this.error = error;
}
static ok(value) {
return new Result(true, value, null);
}
static fail(error) {
return new Result(false, null, error);
}
}
/* ----------------------------------------------------------------------- */
/* 3. Strategy de autenticação — permite trocar Bearer/API Key/OAuth sem */
/* alterar o cliente HTTP (Open/Closed Principle). */
/* ----------------------------------------------------------------------- */
class AuthStrategy {
/** @returns {Record<string, string>} headers a serem aplicados na requisição */
getHeaders() {
throw new Error("getHeaders() deve ser implementado pela estratégia concreta");
}
}
class BearerTokenAuth extends AuthStrategy {
constructor(token) {
super();
this._token = token;
}
getHeaders() {
return { Authorization: `Bearer ${this._token}` };
}
}
class ApiKeyAuth extends AuthStrategy {
constructor(apiKey, headerName = "x-api-key") {
super();
this._apiKey = apiKey;
this._headerName = headerName;
}
getHeaders() {
return { [this._headerName]: this._apiKey };
}
}
/* ----------------------------------------------------------------------- */
/* 4. HttpClient — única camada que conhece o `fetch`. Responsabilidade */
/* única: fazer a requisição HTTP e normalizar erros. Nada de regra */
/* de negócio aqui. */
/* ----------------------------------------------------------------------- */
class HttpClient {
/**
* @param {AuthStrategy} authStrategy
* @param {number} timeoutMs
*/
constructor(authStrategy, timeoutMs = 8000) {
this._authStrategy = authStrategy;
this._timeoutMs = timeoutMs;
}
/**
* @param {string} url
* @param {"GET"|"POST"|"PUT"|"DELETE"} method
* @param {object|null} body
* @returns {Promise<Result>}
*/
async request(url, method, body = null) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this._timeoutMs);
try {
const response = await fetch(url, {
method,
headers: {
"Content-Type": "application/json",
...this._authStrategy.getHeaders()
},
body: body ? JSON.stringify(body) : null,
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.status === 401 || response.status === 403) {
return Result.fail(new UnauthorizedError(response.status));
}
if (!response.ok) {
return Result.fail(
new ApiError(`API retornou status ${response.status}`, { status: response.status })
);
}
const data = await response.json();
return Result.ok(data);
} catch (error) {
clearTimeout(timeoutId);
return Result.fail(new NetworkError(error));
}
}
}
/* ----------------------------------------------------------------------- */
/* 5. Camada de domínio — cada serviço conhece apenas o seu endpoint e o */
/* formato do payload. Não sabe nada sobre HTTP, fetch ou autenticação. */
/* ----------------------------------------------------------------------- */
class OfferValidationService {
/**
* @param {HttpClient} httpClient
* @param {string} baseUrl
*/
constructor(httpClient, baseUrl) {
this._httpClient = httpClient;
this._baseUrl = baseUrl;
}
/**
* @param {{ clienteId: string, planoOferta: string }} dados
* @returns {Promise<Result>} value esperado: { valido: boolean, planoAtual?: string }
*/
async validar(dados) {
return this._httpClient.request(`${this._baseUrl}/oferta/validar`, "POST", dados);
}
}
class OrderService {
/**
* @param {HttpClient} httpClient
* @param {string} baseUrl
*/
constructor(httpClient, baseUrl) {
this._httpClient = httpClient;
this._baseUrl = baseUrl;
}
/**
* @param {{ clienteId: string, planoOferta: string, idempotencyKey: string }} dados
* @returns {Promise<Result>}
*/
async criar(dados) {
return this._httpClient.request(`${this._baseUrl}/order`, "POST", dados);
}
}
/* ----------------------------------------------------------------------- */
/* 6. Facade — orquestra o caso de uso completo (validar → postar order) */
/* Essa é a única classe que conhece a ordem das etapas do negócio. */
/* ----------------------------------------------------------------------- */
class ContratacaoOfertaUseCase {
/**
* @param {OfferValidationService} offerValidationService
* @param {OrderService} orderService
*/
constructor(offerValidationService, orderService) {
this._offerValidationService = offerValidationService;
this._orderService = orderService;
}
/**
* @param {{ clienteId: string, planoOferta: string, idempotencyKey: string }} dados
* @returns {Promise<Result>}
*/
async executar(dados) {
const validacao = await this._offerValidationService.validar({
clienteId: dados.clienteId,
planoOferta: dados.planoOferta
});
if (!validacao.isSuccess) {
return Result.fail(validacao.error);
}
if (!validacao.value.valido) {
return Result.fail(new ApiError("Oferta inválida ou expirada"));
}
const order = await this._orderService.criar(dados);
if (!order.isSuccess) {
return Result.fail(order.error);
}
return Result.ok(order.value);
}
}
/* ----------------------------------------------------------------------- */
/* 7. Composição e uso — único lugar onde as peças são "montadas" */
/* (Dependency Injection manual, sem framework). */
/* ----------------------------------------------------------------------- */
async function main() {
const auth = new BearerTokenAuth("SEU_TOKEN_AQUI");
const httpClient = new HttpClient(auth);
const offerValidationService = new OfferValidationService(
httpClient,
"https://api.suaempresa.com"
);
const orderService = new OrderService(httpClient, "https://api.suaempresa.com");
const contratarOferta = new ContratacaoOfertaUseCase(offerValidationService, orderService);
const resultado = await contratarOferta.executar({
clienteId: "12345",
planoOferta: "premium-anual",
idempotencyKey: crypto.randomUUID()
});
if (resultado.isSuccess) {
console.log("Pedido criado com sucesso:", resultado.value);
} else {
console.error(`Falha no fluxo (${resultado.error.name}):`, resultado.error.message);
}
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment