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 frequentemente têm endpoints com respostas variáveis. Defaults permitem:
- retorno
unknownpor 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.unknownobriga 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
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
- Liste os genéricos expostos na sua API pública (não os internos).
- Identifique quais são usados no caso comum e quais só em cenários avançados.
- Para os avançados, adicione valores padrão que sejam:
- seguros (preferir
unknownaany); - úteis (erro mínimo com
message); - consistentes (um único padrão por biblioteca).
- seguros (preferir
- Reescreva exemplos da documentação para o caminho simples:
- exemplo sem genéricos;
- exemplo com 1 genérico;
- exemplo completo (avançado).
- 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.