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/xvira uma chamada ao endpointoutro/xdo 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: oXMLHttpRequestnã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();
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 volta | Quer dizer | O 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_session | O 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 forbidden | Quem usa não tem o papel que o endpoint pede. | Diga que a pessoa não tem permissão. |
404 not_found | Nã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 outros | Sã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 internal | O 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.
workflow é só aparênciaEsconder 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.