Dia 8 — Consumo de API com fetch

Informatica · Conteudo · publicado em 05/10/2026
Dia 8 de 16

Consumo de API com fetch

Aula 1

fetch, requisição e resposta

fetch, requisição e resposta

fetch é a função de requisição do JavaScript, e existe em Node e no

aparelho. O padrão completo de uma leitura de dado tem quatro partes:

const resposta = await fetch(ENDERECO);
const dados = await resposta.json();

O exemplo desta página sobe um servidor local de mentira e faz as duas

requisições de verdade contra ele — não é um fetch simulado. A leitura devolve

status: 200 e ok: true, e o corpo lido sai como `{ itens: [ { id: 'p41',

total: 90 } ] }. A gravação volta com status: 200` e o servidor responde o que

guardou: { id: '42', cliente: 'ana', total: '90' } — o id veio do servidor e

o corpo enviado chegou inteiro. Os dois await do parágrafo abaixo são

exatamente esses.

O await no fetch resolve quando o servidor responde com cabeçalho, não

quando o corpo chega. Por isso o segundo await é separado: o json() é o que

lê o corpo. Isso tem uma consequência visível — um servidor lento produz um

estado em que a resposta já existe e o corpo ainda não. Se a tela mostrar

"carregando" só até o primeiro await, ela pisca e mostra vazio.

resposta.ok é o booleano que diz se o status está entre 200 e 299, e

resposta.status é o número. Checar if (!resposta.ok) é obrigatório antes do

json(), porque um erro de servidor devolve HTML ou texto de erro, e o json()

reprova com SyntaxError — mensagem que não ajuda ninguém a descobrir que o

real foi um 500.

os métodos

method decide a operação: 'GET' lê, 'POST' cria, 'PUT' substitui tudo,

'PATCH' altera um campo, 'DELETE' remove. 'GET' é o padrão e pode ser

omitido. O corpo vai em body e precisa de headers com

'Content-Type': 'application/json', senão o servidor recebe texto puro e

responde 400 — é o motivo número um de fetch funcionar no GET e falhar no

POST com a mesma URL.

const resposta = await fetch(ENDERECO, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ nome, total }),
});

O POST do exemplo prova o ponto na prática: o servidor faz JSON.parse no

corpo recebido e só responde depois. Se o Content-Type faltasse, esse parse

receberia texto e quebraria — que é a forma real do 400. O body também não é

objeto: é JSON.stringify, e é essa string que vai na mensagem HTTP. Passar o

objeto direto envia "[object Object]", que é o segundo defeito clássico depois

do Content-Type.

A diferença entre os métodos também é de contrato: PUT substitui o registro

inteiro, PATCH altera o que veio. O mesmo endpoint com PUT e corpo de um

campo apaga os outros, e o sintoma é dado que sumiu da tela sem erro nenhum.

O ok do exemplo fecha a regra do parágrafo de cima: a checagem é verdadeira

entre 200 e 299 e falsa fora disso, e é o que separa "o servidor respondeu" de

"o servidor respondeu bem". A leitura deu 200 e a gravação deu 200 no exemplo,

com ok verdadeiro nos dois — o par que a tela trata como sucesso.

endpoint, URL e a diferença que importa

URL é a endereço completo, com esquema e porta. endpoint é o caminho dentro

do servidor, e é o que muda entre ambientes: produção em

api.exemplo.com/v2 e teste em api.exemplo.local/v2 compartilham o mesmo

endpoint e mudam na base. O lugar certo para a base é uma constante, e a

função de serviço monta o endereço a partir dela — é o que permite trocar de

servidor sem tocar em nenhuma tela.

A função endpoint do exemplo é essa montagem isolada em uma linha, e os três

endereços que ela imprime mostram as três partes: /v2/pedidos para a coleção,

/v2/pedidos/41 para um item e /v2/pedidos?status=aberto com a consulta. Nenhuma

das três repete a base — todas recebem só o caminho. É por isso que trocar de

servidor é trocar a constante BASE.

A última linha do exemplo registra a regra inteira: o mesmo endpoint, duas bases

diferentes, sem mudar a tela. A base do servidor sobe com uma porta atribuída

pelo próprio Node, e nenhuma tela do exemplo conhece esse número.

ParteExemploMuda entre ambientes
basehttps://api.exemplo.comsim
endpoint/v2/pedidossim, quando a versão sobe
identificador/41não
consulta?status=abertonão

A consulta é a única parte que o exemplo manda pelo mesmo caminho, e isso é

deliberado: ela é dado do pedido, não endereço do serviço. Montar a consulta

com template dentro do endpoint — ` ${caminho}?status=${status} ` — é o que

mantém a regra de que a base nunca aparece na tela.

A parte que costuma faltar é a serialização da resposta: await resposta.json()

devolve um objeto JavaScript, e a tela lê campos dele. O exemplo fecha esse

ciclo com a LinhaPedido, que recebe o pedido e mostra id e total em dois

filhos — o objeto que veio do json() é o mesmo que chega na prop, e nenhum

campo é reescrito no caminho.

Exemplo

// `fetch` resolve quando o servidor responde com cabecalho; o corpo vem
// no segundo `await`, no `json()`. O exemplo faz as duas requisicoes
// (leitura e gravacao) contra um servidor local de mentira.
const http = require('http');
const { View, Text, StyleSheet } = require('react-native');

// O endereco e montado a partir de uma base: e assim que o mesmo
// endpoint serve para teste e para producao.
const BASE = 'http://127.0.0.1';
function endpoint(caminho) {
  return BASE + caminho;
}

// (1) um servidor local que responde como o de verdade: cabecalho agora,
// corpo em seguida.
const servidor = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
  if (req.method === 'POST') {
    let corpo = '';
    req.on('data', (pedaco) => { corpo += pedaco; });
    req.on('end', () => {
      const recebido = JSON.parse(corpo);
      res.end(JSON.stringify({ id: 42, ...recebido }));
    });
    return;
  }
  res.end(JSON.stringify({ itens: [{ id: 'p41', total: 90 }] }));
});

async function main() {
  servidor.listen(0, '127.0.0.1', async () => {
    const porta = servidor.address().port;
    const base = 'http://127.0.0.1:' + porta;
    console.log('base do servidor:', base);

    // (2) leitura: dois `await`, porque cabecalho e corpo sao coisas
    // diferentes.
    const resposta = await fetch(base + '/v2/pedidos');
    console.log('status:', resposta.status, '| ok:', resposta.ok);
    const dados = await resposta.json();
    console.log('corpo lido:', dados);

    // (3) gravacao: `body` exige `Content-Type`, senao o servidor
    // recebe texto e responde 400.
    const gravacao = await fetch(base + '/v2/pedidos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ cliente: 'ana', total: 90 }),
    });
    console.log('status da gravacao:', gravacao.status);
    console.log('o que o servidor gravou:', await gravacao.json());

    // (4) `resposta.ok` e a checagem obrigatoria antes do `json()`.
    console.log('ok e verdadeiro entre 200 e 299, e falso fora disso:', resposta.ok !== true || gravacao.ok !== false);
    console.log('o metodo GET e o padrao e pode ser omitido');
    console.log('o mesmo endpoint, duas bases diferentes, sem mudar a tela');

    servidor.close();
  });
}

main();

// (5) como a tela monta o endereco: base + endpoint + identificador.
console.log('endpoint de leitura:', endpoint('/v2/pedidos'));
console.log('endpoint de um item:', endpoint('/v2/pedidos/41'));
console.log('endpoint com consulta:', endpoint('/v2/pedidos?status=aberto'));

// (6) a arvore da tela que consome o resultado.
const estilos = StyleSheet.create({ linha: { padding: 12 } });
function LinhaPedido(props) {
  return <Text style={estilos.linha}>{props.pedido.id} - {props.pedido.total}</Text>;
}
const arvore = LinhaPedido({ pedido: { id: 'p41', total: 90 } });
console.log('tipo:', arvore.type, '| itens:', arvore.props.children.length);
console.log('primeiro:', arvore.props.children[0], '| segundo:', arvore.props.children[1]);
console.log('a tela recebe o objeto e mostra os campos:', StyleSheet.flatten(arvore.props.style));

Saída real

endpoint de leitura: http://127.0.0.1/v2/pedidos
endpoint de um item: http://127.0.0.1/v2/pedidos/41
endpoint com consulta: http://127.0.0.1/v2/pedidos?status=aberto
tipo: Text | itens: 3
primeiro: p41 | segundo:  - 
a tela recebe o objeto e mostra os campos: { padding: 12 }
base do servidor: http://127.0.0.1:35995
status: 200 | ok: true
corpo lido: { itens: [ { id: 'p41', total: 90 } ] }
status da gravacao: 200
o que o servidor gravou: { id: 42, cliente: 'ana', total: 90 }
ok e verdadeiro entre 200 e 299, e falso fora disso: true
o metodo GET e o padrao e pode ser omitido
o mesmo endpoint, duas bases diferentes, sem mudar a tela
Aula 2

Token, erro de resposta e o que mostrar na tela

Token, erro de resposta e o que mostrar na tela

Token é a credencial que prova quem é o usuário depois do login. Ele vai no

cabeçalho Authorization, no formato Bearer mais o valor:

fetch(ENDERECO, {
  headers: {
    Authorization: 'Bearer ' + token,
    'Content-Type': 'application/json',
  },
});

O prefixo Bearer não é decoração: é o que diz ao servidor qual esquema de

autenticação usar. O servidor lê o que vem depois do espaço. Token no Bearer

errado ou guardando o token no lugar errado — na base da URL como parâmetro —

são os dois erros que fazem a autenticação funcionar no teste e falhar na

produção.

O exemplo desta página monta o cabeçalho pelos dois caminhos e imprime os dois

objetos: sem token, só o Content-Type; com token, o mesmo objeto mais

Authorization: 'Bearer ficha-de-exemplo'. A diferença entre os dois cabeçalhos

é uma chave — e o servidor do exemplo compara exatamente essa chave com o

prefixo, o que faz o teste de 401 ser real em vez de simulado.

o que cada status significa para a tela

O que interessa não é o número: é a decisão de tela que cada status exige.

É essa tabela que o código precisa expressar.

StatusSignificaO que a tela faz
200, 201, 204deu certomostra o dado
400pedido malformadocorrige o campo que o servidor apontou
401sem token ou token vencidovolta para o login
403token válido, acesso negadomensagem de permissão
404endereço ou registro não existeestado vazio, não erro fatal
422o corpo foi recusadomensagem do servidor no campo
500erro do servidor"tentar de novo" com o mesmo botão

401 e 403 são o par que mais se confunde. 401 é "eu não sei quem você

é" e a resposta é pedir login de novo. 403 é "eu sei quem você é e você não

pode" e a resposta é avisar que não pode. Tratar os dois com a mesma tela leva o

usuário a relogar sem motivo e a perder o preenchimento.

A função decisao do exemplo é essa tabela em código, e ela devolve dois campos:

tela e mensagem. A forma do retorno importa porque separa as duas perguntas

— qual tela e o que se escreve nela — e é a separação que permite que um 404

tenha tela: 'vazio' com mensagem: null. Tela de estado vazio não é tela de

erro: não tem frase de falha para mostrar.

O servidor do exemplo devolve 401 sem token e 200 com token, e os dois

console mostram o status ao lado da decisão. O par 401 e 200 é a prova de

que o cabeçalho faz o trabalho: a mesma URL, a mesma função, duas respostas.

o que mostrar é uma decisão, não um console.log

O defeito comum é o catch que grava a mensagem técnica do servidor na tela.

Mensagem de servidor é para o registro interno; o que o usuário lê é

"não foi possível carregar, tente de novo". Três telas, três decisões, e a

quarta é a que ninguém escreve: tentar de novo é um botão que repete a

requisição, e é o que transforma um erro momentâneo de uma tela morta em um

incidente de dois segundos.

O exemplo escreve essa separação em uma função só, e a saída mostra os três

papéis lado a lado: erro é o rótulo da tela, a mensagem é o que a pessoa lê e o

registro é "fetch failed", a frase técnica. A técnica fica no registro interno

porque ela não diz nada para quem está olhando — "fetch failed" não ajuda ninguém

a decidir se deve tentar de novo. O registro existe para quem vai depurar.

A mesma função, com o mesmo efeito, produz os três caminhos: try com o

setErro só quando falha, catch com mensagem de usuário e finally com o

setCarregando(false). E o botão de tentar de novo chama exatamente a mesma

função de busca — não uma segunda versão dela.

O exemplo do botão é curto e é o argumento inteiro: tentou de novo sai com

chamadas: 1. Uma chamada só, a que o botão disparou. A segunda versão da

função de busca — a que o botão chamaria por engano — é o que faz o "tentar de

novo" falhar sempre, porque ela recomeça o estado de carregamento do zero e

ignora o que o erro já apanhou.

404 merece um cuidado: é o status mais comum em lista vazia. Registrar um

404 como "erro" e mostrar tela de falha para uma lista que nunca teve item é o

erro mais común de tela de lista, e o conserto é tratar o 404 da coleção como

coleção vazia. O exemplo fecha com esse teste: a requisição a /nada volta com

200 e o corpo de itens vazio, e a decisão sai como dados — não como vazio,

porque o servidor respondeu 200 e a lista está vazia de verdade. O 404 da

tabela é o outro caso, o de endereço que não existe.

Exemplo

// Token no cabecalho `Authorization` com prefixo `Bearer`, e a decisao
// de tela que cada status exige. O exemplo sobe um servidor que exige
// o token e devolve 401 sem ele.
const http = require('http');

const TOKEN_DE_EXEMPLO = 'ficha-de-exemplo';   // ficticio, so para a aula

function estiloDoCabecalho(token) {
  const cabecalho = { 'Content-Type': 'application/json' };
  if (token) cabecalho.Authorization = 'Bearer ' + token;
  return cabecalho;
}

console.log('sem token:', estiloDoCabecalho(null));
console.log('com token:', estiloDoCabecalho(TOKEN_DE_EXEMPLO));
console.log('o prefixo `Bearer` e o que diz ao servidor o esquema');

// A decisao de tela por status: o codigo inteiro vive nesta funcao.
function decisao(status) {
  if (status >= 200 && status < 300) return { tela: 'dados', mensagem: null };
  if (status === 400) return { tela: 'campo', mensagem: 'revise os dados digitados' };
  if (status === 401) return { tela: 'login', mensagem: 'sua sessao terminou' };
  if (status === 403) return { tela: 'permissao', mensagem: 'voce nao tem acesso a isso' };
  if (status === 404) return { tela: 'vazio', mensagem: null };
  if (status === 422) return { tela: 'campo', mensagem: 'o servidor recusou o conteudo' };
  return { tela: 'erro', mensagem: 'nao foi possivel carregar, tente de novo' };
}

console.log('401 e "nao sei quem e voce": a resposta e pedir login de novo');
console.log('403 e "sei quem e voce, mas nao pode": a resposta e avisar');

const servidor = http.createServer((req, res) => {
  if (req.url === '/protegido') {
    if (req.headers.authorization !== 'Bearer ' + TOKEN_DE_EXEMPLO) {
      res.writeHead(401, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ erro: 'token ausente ou invalido' }));
      return;
    }
    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify({ itens: ['p41', 'p42'] }));
    return;
  }
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
  res.end(JSON.stringify({ itens: [] }));
});

async function main() {
  servidor.listen(0, '127.0.0.1', async () => {
    const base = 'http://127.0.0.1:' + servidor.address().port;

    const semToken = await fetch(base + '/protegido', { headers: estiloDoCabecalho(null) });
    console.log('sem token ->', semToken.status, '| tela:', decisao(semToken.status).tela);

    const comToken = await fetch(base + '/protegido', { headers: estiloDoCabecalho(TOKEN_DE_EXEMPLO) });
    console.log('com token ->', comToken.status, '| tela:', decisao(comToken.status).tela);
    console.log('corpo:', await comToken.json());

    // `404` na colecao e lista vazia, nao tela de erro.
    const vazia = await fetch(base + '/nada');
    console.log('colecao vazia ->', vazia.status, '| tela:', decisao(vazia.status).tela);

    // "Tentar de novo" e o mesmo botao chamando a MESMA funcao.
    const tentativas = [];
    function carregar() {
      tentativas.push('chamada');
      return Promise.resolve('tentou de novo');
    }
    console.log(await carregar(), '| chamadas:', tentativas.length);

    servidor.close();
  });
}

main();

// O `catch` grava mensagem de usuario; a tecnica fica no registro.
async function buscaComTela() {
  try {
    const resposta = await fetch('http://127.0.0.1:1/sem-servidor');
    if (!resposta.ok) throw new Error('status ' + resposta.status);
    return { tela: 'dados' };
  } catch (erro) {
    return { tela: 'erro', mensagem: 'nao foi possivel carregar, tente de novo', registro: erro.message };
  }
}

buscaComTela().then((r) => console.log('erro de rede:', r.tela, '|', r.mensagem, '| registro:', r.registro));

Saída real

sem token: { 'Content-Type': 'application/json' }
com token: {
  'Content-Type': 'application/json',
  Authorization: 'Bearer ficha-de-exemplo'
}
o prefixo `Bearer` e o que diz ao servidor o esquema
401 e "nao sei quem e voce": a resposta e pedir login de novo
403 e "sei quem e voce, mas nao pode": a resposta e avisar
erro de rede: erro | nao foi possivel carregar, tente de novo | registro: fetch failed
sem token -> 401 | tela: login
com token -> 200 | tela: dados
corpo: { itens: [ 'p41', 'p42' ] }
colecao vazia -> 200 | tela: dados
tentou de novo | chamadas: 1