Integração com API de compras públicas sem testar em produção nem quebrar no leiaute
Monte uma integração com dados de compras públicas que lê o contrato publicado, separa consulta de envio, testa com dado fictício e avisa quando o leiaute muda.
A empresa vende equipamento de informática para prefeituras e quer um "radar de licitações". O desenvolvedor abre a API de dados abertos, copia um identificador de processo real da tela do portal, chama o serviço de produção para "ver o JSON" e monta o parser em cima daquela resposta. Três meses depois, um campo deixa de vir preenchido e o radar para de avisar sem dar erro nenhum.
A resposta curta: trate a API pública como um contrato versionado, não como uma tela para raspar. Leia o manual da versão que você usa e guarde uma cópia dele, separe consulta de dados abertos de qualquer envio oficial, valide toda resposta contra o formato que o seu código espera e teste com dado fictício ou com fixtures gravadas. Quando o formato mudar, você quer um alerta, não um silêncio.
Por que integração com dado público quebra sem avisar
Serviços de governo mudam de endereço, de versão e às vezes de portal. A fonte oficial de um mesmo dado pode migrar, por exemplo, entre portais de dados abertos e o Portal Nacional de Contratações Públicas (PNCP), que tem a própria API. Quem escreveu o parser "no olho", a partir de uma resposta, não sabe quais campos são obrigatórios, quais podem vir nulos e qual é a regra de paginação.
Existe ainda a confusão entre dois mundos. Consulta é leitura de dado público: qualquer um pode fazer, não precisa de credencial, e errar custa pouco. Envio é quando um órgão ou fornecedor publica ou transmite informação oficial: exige credencial, tem efeito jurídico e, quando o serviço oferece ambiente de homologação ou treinamento, é lá que se testa. Misturar os dois no mesmo cliente HTTP, com a mesma configuração, é como alguém acaba mandando teste para produção.
Raspar a tela do portal parece mais rápido e é pior em tudo: o HTML muda sem aviso, a carga recai sobre um site que não foi feito para isso e você perde a documentação que a API oferece. Se o dado existe em API ou em arquivo de dados abertos, use a API ou o arquivo.
Como montar a integração, passo a passo
O exemplo: o radar da Loja Exemplo, um serviço Node.js que roda uma vez por dia, consulta contratações publicadas no período e grava num banco próprio as que interessam.
- Identifique a fonte oficial atual do dado que você precisa e baixe o manual. Guarde o PDF ou o link versionado na pasta
docs/do repositório, com a data. - Liste os campos de que o seu sistema depende e o que o manual diz sobre cada um: tipo, obrigatoriedade, formato de data, unidade de valor.
- Escreva um cliente só de consulta, com URL base em configuração, timeout, nova tentativa limitada para 429 e 5xx e identificação no User-Agent.
- Valide a resposta com um schema. Registro fora do formato vai para uma fila de revisão, não para o banco.
- Grave fixtures de respostas públicas para os testes automatizados. Testes não chamam a API.
- Consulte de forma incremental, por janela de datas, e guarde o resultado. A tela do seu sistema lê o seu banco, nunca a API a cada clique.
- Rode uma verificação de contrato diária contra a API real e alerte quando a validação falhar acima de um limite.
Um cliente de consulta que aguenta instabilidade
import { z } from 'zod';
const BASE_URL = process.env.COMPRAS_API_URL; // URL base do manual, terminando em "/"
const MAX_TENTATIVAS = 4;
const esperar = (ms) => new Promise((r) => setTimeout(r, ms));
export async function consultar(caminho, params, tentativa = 1) {
const url = new URL(caminho, BASE_URL); // caminho sem "/" inicial, copiado do manual
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v));
let resp;
try {
resp = await fetch(url, {
headers: {
Accept: 'application/json',
'User-Agent': 'LojaExemplo-Radar/1.0 (ti@lojaexemplo.com.br)'
},
signal: AbortSignal.timeout(15000)
});
} catch (err) {
if (tentativa < MAX_TENTATIVAS) {
await esperar(2 ** tentativa * 1000);
return consultar(caminho, params, tentativa + 1);
}
throw err;
}
if ((resp.status === 429 || resp.status >= 500) && tentativa < MAX_TENTATIVAS) {
const retryAfter = Number(resp.headers.get('retry-after'));
await esperar(retryAfter > 0 ? retryAfter * 1000 : 2 ** tentativa * 1000);
return consultar(caminho, params, tentativa + 1);
}
if (!resp.ok) throw new Error('Consulta falhou: HTTP ' + resp.status + ' em ' + url.pathname);
return resp.json();
}
Validar o que chega antes de gravar
Os nomes de campo abaixo são ilustrativos; copie os do manual da versão que você integra. O ponto é a técnica: o schema descreve exatamente o que o seu código usa, e o mapeamento para o seu domínio fica num lugar só.
const ContratacaoApi = z.object({
identificador: z.string().min(1),
objeto: z.string(),
uf: z.string().length(2),
valorEstimado: z.number().nullable(),
dataPublicacao: z.string().date()
});
export function mapear(registros) {
const validos = [];
const rejeitados = [];
for (const bruto of registros) {
const r = ContratacaoApi.safeParse(bruto);
if (r.success) {
validos.push({
idExterno: r.data.identificador,
objeto: r.data.objeto,
uf: r.data.uf,
valorCentavos: r.data.valorEstimado === null ? null : Math.round(r.data.valorEstimado * 100),
publicadoEm: r.data.dataPublicacao
});
} else {
rejeitados.push({ bruto, erro: r.error.issues.map((i) => i.path.join('.')).join(', ') });
}
}
return { validos, rejeitados };
}
Valor em centavos e inteiro evita a soma que não fecha no relatório; o porquê está em dinheiro em centavos. A gravação usa o idExterno como chave única, com upsert, para que rodar o job duas vezes no mesmo dia não duplique registros. Se a API pagina, siga a regra de paginação do manual e trate a página vazia como fim; os cuidados estão em paginação que não pula.
Teste com fixture, não com a API
Dado de consulta é público, então gravar uma resposta real como fixture é aceitável. Salve em test/fixtures/consulta-2026-10.json, junto com uma variação editada à mão com um campo nulo e outro ausente. O teste chama mapear e confere quantos registros passam e quais são rejeitados. Para qualquer fluxo de envio, nada de identificador real: use dado fictício e o ambiente de teste que o serviço oferecer.
A verificação de contrato diária
Um job separado consulta um dia recente e calcula a proporção de rejeitados. Se passar de, digamos, 5%, abre um alerta com os caminhos de campo que falharam. Foi isso que faltou no radar do início: a mudança teria virado um aviso na manhã seguinte, com o nome do campo, em vez de três semanas de silêncio.
Erros comuns
- Testar com identificador real em serviço de envio. O teste vira transmissão oficial, com efeito que você não desfaz com um rollback.
- Escrever o parser a partir de uma resposta, sem o manual. Campo opcional vira obrigatório no seu código e a integração quebra no primeiro registro diferente.
- Chamar a API a cada acesso à tela. O sistema fica lento, depende da disponibilidade do serviço público e pode esbarrar em limite de requisições.
- Ignorar 429 e tentar de novo em laço. Você amplia a sobrecarga e pode ter o acesso bloqueado. Respeite o Retry-After e limite as tentativas, como em timeout antes do infinito.
- Colocar credencial de envio no front ou no mesmo arquivo da consulta. Quem abre o navegador leva a credencial, e um job de teste pode usá-la por engano.
- Raspar a tela do portal. O HTML muda sem aviso e sem versão, e você perde a documentação que existe para a API.
Checklist
- O manual da versão integrada está em
docs/, com data. - A URL base vem de configuração, separada para consulta e envio.
- Toda chamada tem timeout e número máximo de tentativas.
- Toda resposta passa por schema antes de chegar ao banco.
- Os testes automatizados usam fixtures e não fazem chamada externa.
- Nenhum teste de envio usa identificador de processo real.
- Existe alerta para aumento de registros rejeitados.
Se a sua integração também gera arquivos para envio, o cuidado com a versão do leiaute está em XML de compras públicas: a versão do leiaute é parte do arquivo. Isto não ensina a licitar, nem substitui a orientação jurídica de quem participa do processo. Abra agora o arquivo de configuração dos seus testes: a chamada usa dado fictício ou um identificador real?
Referência: https://api.compras.dados.gov.br/