
Capivara — o app de pedidos e o duplo /api que devolvia 404
A ideia: anotar o que eu quero, no lugar onde eu já moro
O Capivara é o hub onde tudo do meu ecossistema passa — projetos, saúde, backups, alertas. Mas tinha uma coisa que vivia fora dele: a lista de pedidos. Cada grupo (Arachne, Dogwalk, mídia, segurança, infra) tem seus pedidos espalhados em conversas, issues e notas soltas. Quando eu queria lembrar “o que eu pedi mesmo pro Arachne?”, eu tinha que caçar em três lugares.
A pergunta que destravou foi simples: por que não ter um lugar único pra anotar cada pedido, com grupo, prioridade e status, e acompanhar o ciclo de vida inteiro?
Nasceu o App de Pedidos — um CRUD completo dentro do admin do Capivara.
O backend: um router que não deixa passar nada errado
A API é enxuta: GET, POST, PATCH e DELETE em /api/pedidos, tudo com whitelist estrita de valores válidos no servidor:
GRUPOS_VALIDOS = [
"arachne", "capivara", "dogwalk", "media", "portifolio",
"yurumi", "seguranca", "infra", "geral"
]
STATUS_VALIDOS = ["pendente", "em_andamento", "concluido", "blocked"]
O modelo guarda o essencial: texto do pedido, grupo, prioridade (baixa, media, alta, urgente), quem criou e timestamps. E tem um detalhe que eu gosto — quando o status vira concluido, o backend grava o concluido_em automaticamente:
if body.status == "concluido":
pedido.concluido_em = datetime.now(timezone.utc)
Tudo é admin-only: sem is_admin, a resposta é 403 antes mesmo de tocar no banco. E os grupos, prioridades e status são validados de novo no PATCH — não dá pra mandar um grupo inventado.
O bug silencioso: /api/api/pedidos
O frontend tem um helper apiFetch() que já monta o prefixo /api sozinho:
const API = '/api'
async function apiFetch(path: string, options?: RequestInit) {
const token = getToken()
let res = await fetch(`${API}${path}`, {
// ...
})
Repara: apiFetch('/pedidos') vira GET /api/pedidos. Agora olha o que eu escrevi na seção nova do admin, nos quatro métodos (listar, criar, editar, deletar):
apiFetch(`/api/pedidos?${params}`) // vira /api/api/pedidos
apiFetch('/api/pedidos', { method: 'POST' }) // idem
apiFetch('/api/pedidos') → fetch('/api' + '/api/pedidos') → /api/api/pedidos → 404.
O pior: o bug não gritava. A seção abria, chamava o GET, recebia 404, e o catch só mostrava uma mensagem pequena de erro. Nada quebrava com estrondo — o app de pedidos simplesmente parecia vazio. Se você não estivesse esperando ver pedidos, nem percebia que algo estava errado.
A correção: uma letra por chamada
O fix foi cirúrgico — remover o /api duplicado das quatro chamadas:
// antes
apiFetch(`/api/pedidos?${params}`)
// depois
apiFetch(`/pedidos?${params}`)
Mesma coisa no POST, no PATCH e no DELETE. Foi um dos fixes mais rápidos da semana em tempo de edição, e um dos mais frustrantes em tempo de debug — porque o bug não dava stack trace, só um 404 quieto que eu tinha que caçar.
Aprendizados
- Convenção de helper é contrato. Se
apiFetchmonta o prefixo, a regra é: passar o caminho SEM o prefixo. Convenção violada em um lugar vira bug silencioso em todos. - Erro “leve” esconde problema real. O
catchque só mostra uma linha pequena de erro é ótimo pra UX, péssimo pra debug. Vale logar a URL completa que foi chamada — o/api/api/pedidosapareceria na hora. - CRUD com whitelist no servidor é barato e certo. Grupo, prioridade e status validados no backend, não só no dropdown do frontend. É o tipo de validação que impede dado sujo de entrar no banco.
- Feature nova merece um clique de teste no admin real. Os testes unitários passavam porque mockavam o
apiFetch— o duplo/apisó apareceu quando a seção chamou a API de verdade.
O que vem a seguir
O App de Pedidos está no ar e já é o lugar único de anotar pedido. Próximos passos naturais: vincular cada pedido a uma issue ou card de projeto, e um resumo por grupo no dashboard (contagem de pendentes por grupo). O ciclo completo — pedido → em andamento → concluído — já tem o esqueleto pronto.