search

Simplificando APIs Complexas: Parâmetros Genéricos com Valores Padrão

Simplificando APIs Complexas: Parâmetros Genéricos com Valores Padrão

Simplificando APIs Complexas: Parâmetros Genéricos com Valores Padrão

APIs Complexas surgem quando uma biblioteca ou serviço precisa atender muitos cenários: integrações diferentes, tipos variados de dados, múltiplos formatos de resposta, estratégias de autenticação, cache, paginação, tolerância a falhas e assim por diante. Em programação, parte dessa complexidade é “inevitável” (porque o domínio é complexo), mas uma parcela relevante é acidental: causada por escolhas de design que aumentam o atrito para o usuário da API.

Um padrão de design que vem ganhando espaço para reduzir essa complexidade é o uso de parâmetros genéricos com valores padrão (generic type parameters with defaults). A ideia é simples:

  • você expõe o poder da tipagem genérica para quem precisa de controle fino;
  • ao mesmo tempo, fornece padrões seguros e sensatos para o caso comum;
  • resultado: uma API mais fácil de usar, com menos código repetitivo e menor chance de erro.

Este texto explica o conceito, onde ele ajuda de verdade e como aplicá-lo passo a passo, com exemplos em linguagens populares (principalmente TypeScript e também uma visão em outras linguagens), sempre com foco em tornar APIs Complexas mais previsíveis e seguras.


O problema: genéricos “obrigatórios” tornam a API difícil

Genéricos são uma ferramenta poderosa para criar APIs reutilizáveis: Client<TResponse>, Repository<TEntity>, Result<T, E>, etc. Porém, quando a API exige que o consumidor informe genéricos em todas as chamadas, a experiência degrada rapidamente.

Sinais típicos de uma API que ficou pesada:

  • o usuário precisa fornecer 2, 3, 4 genéricos para usar um caso simples;
  • há repetição constante de tipos que poderiam ser inferidos;
  • o “caminho feliz” exige conhecer detalhes internos do design (tipos auxiliares, constraints, unions);
  • erros de tipagem aparecem em cascata, com mensagens difíceis.

Na prática, isso vira um custo de adoção: o usuário prefere uma alternativa “menos correta”, mas mais fácil de usar.


A solução: parâmetros genéricos com valores padrão

Ao permitir que um genérico tenha um valor padrão, você transforma este modelo:

  • “o usuário deve saber qual tipo informar”

em:

  • “o usuário pode informar um tipo quando precisar, mas se não informar, a API assume um padrão”.

Em TypeScript, por exemplo, isso é feito assim:

type ApiResponse<TData = unknown> = {
  data: TData;
  status: number;
};

Se alguém usar ApiResponse sem passar TData, o padrão será unknown. Se precisar, pode passar ApiResponse<User>, e pronto.

O efeito direto em APIs Complexas é reduzir o número de decisões obrigatórias e deixar escolhas avançadas explícitas apenas para casos avançados.


Passo a passo: como aplicar em uma API de cliente HTTP (TypeScript)

Imagine um cliente HTTP genérico. Uma versão “crua” pode exigir múltiplos genéricos:

  • tipo do corpo de resposta
  • tipo do erro
  • tipo do “envelope” (response wrapper)
  • tipo do contexto (ex.: trace IDs)

Isso pode virar algo assim:

type HttpResult<TData, TError, TMeta> =
  | { ok: true; data: TData; meta: TMeta }
  | { ok: false; error: TError; meta: TMeta };

interface HttpClient {
  request<TData, TError, TMeta>(
    path: string,
    options?: Record<string, unknown>
  ): Promise<HttpResult<TData, TError, TMeta>>;
}

Para uma chamada simples, o usuário teria que escrever:

client.request<User, ApiError, RequestMeta>("/me");

Agora, aplique parâmetros genéricos com valores padrão.

1) Defina padrões realistas e seguros

Evite padrões “otimistas” demais. Para dados desconhecidos, unknown é melhor que any porque força validação/parse.

type DefaultMeta = { requestId?: string };
type DefaultError = { message: string; code?: string };

type HttpResult<TData = unknown, TError = DefaultError, TMeta = DefaultMeta> =
  | { ok: true; data: TData; meta: TMeta }
  | { ok: false; error: TError; meta: TMeta };

2) Atualize a assinatura para aceitar os padrões

interface HttpClient {
  request<TData = unknown, TError = DefaultError, TMeta = DefaultMeta>(
    path: string,
    options?: Record<string, unknown>
  ): Promise<HttpResult<TData, TError, TMeta>>;
}

3) O “caso comum” fica simples

const res = await client.request("/health");
if (res.ok) {
  // res.data é unknown; o chamador decide se precisa tipar/validar
}

4) O caso tipado continua disponível, mas opcional

type User = { id: string; name: string };

const res = await client.request<User>("/me");
if (res.ok) {
  res.data.name; // tipado
}

5) Casos avançados continuam possíveis sem “poluir” o básico

type MyError = { message: string; code: string; details?: unknown };
type MyMeta = { requestId: string; durationMs: number };

const res = await client.request<User, MyError, MyMeta>("/me");

Esse padrão cria um degrau claro: “use sem genéricos” (simples), “use 1 genérico” (comum), “use 3 genéricos” (avançado).


Onde isso mais ajuda em APIs Complexas

1) SDKs e clientes de API (REST/GraphQL)

SDKs e clientes de API

SDKs frequentemente têm endpoints com respostas variáveis. Defaults permitem:

  • retorno unknown por padrão (segurança);
  • tipagem forte quando desejado;
  • menos sobrecarga cognitiva em endpoints simples.

2) Bibliotecas de validação e parsing

Ao combinar validação com tipos, defaults reduzem fricção:

  • parse<T = unknown>(input) vira uma interface segura;
  • o usuário só tipa quando houver contrato.

3) Ferramentas de observabilidade e telemetria

Contexto e metadados (trace, span, tags) muitas vezes são genéricos. Defaults evitam que cada time “invente” um tipo do zero.

4) Infra como código e automação

Tipos para recursos podem ser genéricos, mas a maioria quer o “padrão do provider”. Defaults ajudam a equilibrar flexibilidade e usabilidade.


Boas práticas: como escolher os valores padrão

Prefira unknown a any

  • any “desliga” o sistema de tipos.
  • unknown obriga o consumidor a fazer narrowing/validação antes de usar.

Defina defaults que representem o contrato mínimo

Para erros, por exemplo, um default como { message: string } já permite logging e exibição, sem assumir um schema complexo.

Não esconda comportamento perigoso atrás de defaults

Em Cyber Segurança, defaults “permissivos” costumam virar vulnerabilidades por acidente. Exemplos de más escolhas:

  • default de autenticação como “sem autenticação” quando a intenção é segura;
  • default que desativa validação de certificado/TLS;
  • default que aceita qualquer origem (CORS) sem necessidade.

Mesmo quando falamos de genéricos, o princípio vale: defaults devem induzir ao uso correto, não ao uso mais fácil porém inseguro.


Armadilhas comuns e como evitar

Armadilhas comuns

1) Defaults “mascaram” a necessidade de validação

Se o retorno padrão é unknown, ótimo. Mas se você colocar any, você pode induzir o usuário a consumir dados não validados como se fossem confiáveis.

Mitigação:

  • use unknown;
  • ofereça helpers: requestJson<T>(), requestAndValidate(schema).

2) Muitos genéricos ainda são muitos

Mesmo com defaults, 5 ou 6 parâmetros genéricos indicam design excessivamente generalista.

Mitigação:

  • agrupe opções em tipos compostos;
  • use overloads (quando a linguagem suportar bem);
  • crie um “tipo de configuração” único (ClientConfig<TAuth, TMeta = ...>).

3) Defaults inconsistentes criam surpresa

Se request<TData=unknown> usa unknown, mas get<T=any> usa any, o usuário perde previsibilidade.

Mitigação:

  • documente defaults e padronize em toda a API;
  • mantenha um “núcleo” comum de tipos (DefaultError, DefaultMeta).

Uma visão rápida em outras linguagens

  • C++: templates com argumentos padrão existem há décadas e são amplamente usados para reduzir verbosidade em tipos complexos.
  • Rust: não possui “default type parameters” tão universalmente quanto outras linguagens em todas as situações, mas usa defaults em alguns contextos (ex.: parâmetros genéricos em traits podem suportar defaults), e o ecossistema tende a preferir inferência e builders para ergonomia.
  • Java: não tem valores padrão para genéricos; soluções comuns são overloads, builders e tipos auxiliares.
  • Kotlin: não tem “default generic type params” como feature central, mas oferece inferência forte, overloads e parâmetros com default em funções (o que reduz parte do problema).
  • TypeScript: é um dos ambientes onde defaults em genéricos trazem retorno imediato, especialmente em SDKs e bibliotecas.

O ponto não é “qual linguagem é melhor”, e sim reconhecer que, quando a linguagem permite, defaults em genéricos são uma ferramenta direta para reduzir fricção em APIs Complexas.


Checklist prático para aplicar hoje

  1. Liste os genéricos expostos na sua API pública (não os internos).
  2. Identifique quais são usados no caso comum e quais só em cenários avançados.
  3. Para os avançados, adicione valores padrão que sejam:
    • seguros (preferir unknown a any);
    • úteis (erro mínimo com message);
    • consistentes (um único padrão por biblioteca).
  4. Reescreva exemplos da documentação para o caminho simples:
    • exemplo sem genéricos;
    • exemplo com 1 genérico;
    • exemplo completo (avançado).
  5. Audite implicações de segurança:
    • defaults não devem incentivar consumo de dados sem validação;
    • não use defaults que desabilitam proteções (TLS, validação, autenticação).

Conclusão

APIs Complexas não precisam ser difíceis de usar. Parâmetros genéricos com valores padrão permitem uma arquitetura em camadas: o básico é simples, o avançado é possível, e a segurança pode ser preservada por escolhas de defaults mais restritivas e previsíveis.

Para times que mantêm SDKs, bibliotecas internas ou plataformas de integração, essa técnica costuma gerar ganhos rápidos: menos código repetitivo, menos tickets sobre “como tipar isso?”, melhor legibilidade e uma curva de aprendizado mais suave — sem abrir mão da expressividade que tornou os genéricos atraentes em primeiro lugar.

Tags: Cursos
Compartilhar este artigo: