Pular para o conteúdo principal

Chamar a API

Uma Página lê e grava os dados do cliente pelos endpoints dele, com um fetch comum:

fetch('/api/ops/teste-clientes')

O caminho começa sempre com /api/ops/, seguido do caminho do endpoint. O resto é com o construtor e com o app, que fazem a chamada de verdade pela Página (é a ponte):

  • Não escreva o cliente: a ponte o põe. Um /api/ops/outro/x vira uma chamada ao endpoint outro/x do cliente do workflow, nunca a outro cliente.
  • Não escreva o endereço do site: só o caminho, começando com /api/ops/.
  • Não escreva cabeçalho de segurança (o X-CSRF) nem de login: a ponte põe o que precisa.
  • Use o fetch: o XMLHttpRequest não passa pela ponte e é barrado.

Os endpoints de cada cliente são feitos pelo time de desenvolvimento, e o que cada um recebe e responde está no README da pasta do cliente, no workflow-api. Os exemplos abaixo usam o endpoint de teste do cliente Interno, teste-clientes.

Ler​

GET /api/ops/teste-clientes?ativo=true responde a lista dos clientes de teste ativos:

{"clientes":[{"id":7,"nome":"Acme","preco":"1500.00","ativo":true,"updated_at":"2026-10-19T14:03:00.123456Z","updated_by":3}]}

Funciona: uma Página que lê ao abrir e mostra o carregando, a lista, a lista vazia e o erro.

<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
</head>
<body>
<h1>Clientes ativos</h1>
<p id="estado">Carregando…</p>
<ul id="lista"></ul>
<script>
async function carregar() {
const estado = document.getElementById('estado');
try {
const res = await fetch('/api/ops/teste-clientes?ativo=true');
if (!res.ok) {
estado.textContent = res.status === 403
? 'Você não tem permissão para ver os clientes.'
: 'Não foi possível carregar os clientes.';
return;
}
const { clientes } = await res.json();
estado.textContent = clientes.length ? '' : 'Nenhum cliente ativo.';
const lista = document.getElementById('lista');
for (const cliente of clientes) {
const item = document.createElement('li');
const link = document.createElement('a');
link.href = '/cliente?id=' + cliente.id;
link.textContent = cliente.nome + ' — R$ ' + cliente.preco;
item.append(link);
lista.append(item);
}
} catch {
estado.textContent = 'Sem conexão. Tente de novo.';
}
}
carregar();
</script>
</body>
</html>

Ponha o texto que vem da API com textContent, e não dentro de um HTML montado: um nome com < aparece como texto.

Gravar​

POST /api/ops/teste-clientes, com {"nome":"Acme","preco":"1500.00"}, cria e responde 201 com {"id":7}.

Funciona: um formulário que o script envia, mostra o erro e, ao criar, abre a Página do cliente novo.

<form id="novo">
<label>Nome <input name="nome" required maxlength="120"></label>
<label>Preço <input name="preco" required inputmode="decimal" placeholder="1500.00"></label>
<button>Cadastrar</button>
<p id="msg" role="status"></p>
</form>
<script>
const form = document.getElementById('novo');
const msg = document.getElementById('msg');
form.addEventListener('submit', async (evento) => {
evento.preventDefault();
const botao = form.querySelector('button');
botao.disabled = true;
msg.textContent = 'Salvando…';
try {
const res = await fetch('/api/ops/teste-clientes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nome: form.elements.nome.value, preco: form.elements.preco.value }),
});
if (res.status === 201) {
const { id } = await res.json();
location.href = '/cliente?id=' + id;
return;
}
msg.textContent = res.status === 400
? 'Confira o nome e o preço (com ponto e até duas casas: 1500.00).'
: 'Não foi possível cadastrar.';
} catch {
msg.textContent = 'Sem conexão. Tente de novo.';
}
botao.disabled = false;
});
</script>

Mudar e excluir: PATCH /api/ops/teste-clientes/7, com só o que muda, e DELETE /api/ops/teste-clientes/7 respondem 204, sem corpo.

<script>
async function desativar(id) {
const res = await fetch('/api/ops/teste-clientes/' + id, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ativo: false }),
});
return res.status === 204;
}

async function excluir(id) {
const res = await fetch('/api/ops/teste-clientes/' + id, { method: 'DELETE' });
return res.status === 204;
}
</script>

Não funciona:

// O endereço inteiro: a chamada não sai, e o fetch rejeita com TypeError.
fetch('https://algum.endereco/api/ops/teste-clientes');

// Fora de /api/ops/: a chamada não sai.
fetch('/api/me');

// O XMLHttpRequest não passa pela ponte: barrado.
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/ops/teste-clientes');
xhr.send();
A chamada é de verdade, também no construtor

No construtor, a chamada vai com o seu login, no dado de verdade do cliente: gravar no "ver" grava. E o canvas também roda cada Página: uma Página que grava ao abrir grava de novo cada vez que o canvas a mostra. Grave só depois de um clique.

No app e no "ver", uma Página que começa a carregar antes do clique (veja data-href) já faz as leituras, mas o que ela grava espera o clique; sem o clique, nunca sai.

O que volta​

A resposta chega ao HTML como o endpoint a mandou: o status, os cabeçalhos e o corpo. Um 204 não tem corpo: não chame res.json() nele. Um erro vem com o código em inglês, sem mensagem, como {"error":"forbidden"}: o texto que quem usa lê é o seu.

O que voltaQuer dizerO que a Página faz
O fetch rejeita (TypeError)A chamada não saiu: um caminho fora de /api/ops/, o endereço inteiro, ou sem conexão.Confira o caminho; com ele certo, mostre "sem conexão" e deixe tentar de novo.
401 no_sessionO login de quem usa acabou. O construtor e o app levam ao login sozinhos.Nada além de uma mensagem: não chame de novo em laço.
403 forbiddenQuem usa não tem o papel que o endpoint pede.Diga que a pessoa não tem permissão.
404 not_foundNão existe: o caminho ou o método errado, ou o registro (teste-clientes/999). É o mesmo 404 para tudo.Confira o caminho e o método no README do endpoint; para um registro, diga que não foi encontrado.
400, 409 e outrosSão do endpoint: cada um diz no README o que quer dizer (o teste-clientes responde 400 invalid_request a um campo fora das regras).Mostre o que o README do endpoint diz.
500 internalO endpoint falhou.Diga que não foi possível e deixe tentar de novo.

O 403 csrf_required não acontece: a ponte põe o cabeçalho. Um signal de um AbortController cancela a chamada, como num site.

Quem está usando: o workflow​

Antes do primeiro script, a Página ganha um objeto workflow, com o nome e os papéis de quem vê:

  • workflow.name: o nome da pessoa;
  • workflow.roles: os papéis dela no app, como ['leader'].

Funciona: cumprimentar e mostrar o botão só a quem tem o papel.

<p>Olá, <span id="nome"></span>.</p>
<button type="button" id="aprovar" hidden>Aprovar</button>
<script>
document.getElementById('nome').textContent = workflow.name;
if (workflow.roles.includes('leader')) {
document.getElementById('aprovar').hidden = false;
}
</script>

Não funciona:

workflow.roles.push('leader'); // erro: o objeto não muda

let workflow = { roles: ['leader'] }; // troca o nosso pelo seu: a Página deixa de saber quem vê

Quem é "quem vê" depende de onde a Página está: no canvas, você; no "ver", os papéis do Ver como (veja Testar no "ver"); no app, quem usa.

O workflow é só aparência

Esconder um botão não impede a chamada. Quem confere o papel de verdade é o endpoint, no servidor, com os papéis do login. No "ver", o Ver como muda o workflow.roles, mas as chamadas continuam com os seus papéis.