Programação Por Equipe DevBras

Capa do artigo: Integração com API de compras públicas sem testar em produção nem quebrar no leiaute

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.

  1. 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.
  2. 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.
  3. 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.
  4. Valide a resposta com um schema. Registro fora do formato vai para uma fila de revisão, não para o banco.
  5. Grave fixtures de respostas públicas para os testes automatizados. Testes não chamam a API.
  6. 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.
  7. 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?

Avalie este artigo

Referência: https://api.compras.dados.gov.br/