1. De SSR estático para UI viva
Na aula 7 nós renderizamos páginas com EJS: o HTML chega pronto do servidor com os dados embutidos, e o JS no cliente faz pequenas interações com a DOM. Funciona. Mas qualquer ação significativa do usuário (criar registro, favoritar, filtrar) ainda exige recarregar a página inteira.
A aula 8 muda isso: usamos fetch() no navegador para conversar com endpoints JSON do nosso back-end TypeScript+Express. Resultado: a página continua sendo entregue por SSR (rápida, indexável, com sessão), mas ações específicas atualizam só pedaços da tela.
SSR entrega a casa pronta. fetch() + JS são os interruptores e tomadas: o usuário interage com partes da casa sem demolir e reconstruir tudo a cada clique.
Para isso funcionar bem, você precisa entender três blocos:
- Como JS faz coisas assíncronas sem travar a interface (event loop, Promises, async/await).
- Como pedir e enviar dados pela rede (fetch, headers, JSON, erros).
- Como representar estados de UI que dependem de uma resposta que pode demorar ou falhar.
2. Event loop e tarefas
JavaScript é single-threaded. Há uma única thread executando seu código. Então, como o browser consegue fazer várias coisas ao mesmo tempo (animação rolando, request de rede em andamento, usuário digitando)?
A resposta é o event loop. O motor JS tem três estruturas principais:
- Call stack — pilha de funções em execução agora.
- Microtask queue — fila de callbacks de Promises (
.then,awaitresumindo). - Macrotask queue — fila de timers, eventos do DOM, callbacks de
fetchresolvido, I/O.
O ciclo é simples e implacável:
- O JS executa tudo o que está na call stack até esvaziar.
- Esvaziou? Drena todas as microtasks da fila — uma após a outra.
- Pega uma macrotask, coloca na stack, volta ao passo 1.
console.log('1. sync');
setTimeout(() => console.log('4. macrotask'), 0);
Promise.resolve().then(() => console.log('3. microtask'));
console.log('2. sync');
// Saída: 1, 2, 3, 4 — microtasks SEMPRE rodam antes da próxima macrotask
Loops infinitos ou cálculos pesados na call stack travam o event loop e congelam a UI. Por isso operações I/O (rede, disco) são sempre assíncronas — elas não ocupam a thread principal enquanto esperam.
3. Promises
Uma Promise é um objeto que representa um valor que ainda não chegou — um contrato do tipo "vou te entregar este resultado depois, ou te avisar que falhou". Tem três estados:
.then..catch.Uma vez que sai do pending, a Promise nunca volta — fica selada como fulfilled ou rejected.
Métodos de instância
.then(onFulfilled, onRejected?)— callback de sucesso (e opcionalmente de erro)..catch(onRejected)— callback só de erro. Equivalente a.then(undefined, fn)..finally(fn)— roda sempre, sucesso ou erro. Útil para fechar loaders.
fetch('/api/users')
.then(res => res.json())
.then(data => renderUsers(data))
.catch(err => renderError(err))
.finally(() => hideSpinner());
Métodos estáticos
| Método | Comportamento | Quando usar |
|---|---|---|
Promise.all([p1, p2, p3]) |
Resolve quando todas resolvem. Rejeita assim que uma rejeita. | Carregar dados independentes em paralelo (usuário + posts + tags). |
Promise.allSettled([p1, p2, p3]) |
Espera todas, sem falhar. Devolve array de { status, value | reason }. |
Quando você precisa do resultado de todas, mesmo que algumas falhem. |
Promise.race([p1, p2]) |
Resolve/rejeita assim que a primeira resolver/rejeitar. | Implementar timeouts: corre fetch contra um setTimeout. |
Promise.any([p1, p2]) |
Resolve com a primeira que resolver. Só rejeita se todas falharem. | Tentar várias fontes redundantes e ficar com a primeira boa. |
4. async / await
async/await é açúcar sintático em cima de Promises. Não substitui Promise — apenas escreve o mesmo código de uma forma que se lê como código síncrono.
Regras de ouro
async functionsempre retorna uma Promise. Mesmo umreturn 42viraPromise<42>.awaitsó pode aparecer dentro de uma funçãoasync(ou no topo de um ES module).- Erros em
asyncviram exceções normais — usetry/catch. awaitpausa a função, mas não bloqueia a thread. Outras tarefas continuam rodando.
// Promises encadeadas
function carregar(id) {
return fetch(`/api/users/${id}`)
.then(res => res.json())
.then(user => renderUser(user))
.catch(err => mostrarErro(err));
}
// Mesmo código com async/await — mais legível
async function carregar(id) {
try {
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
renderUser(user);
} catch (err) {
mostrarErro(err);
}
}
SyntaxError: await is only valid in async functions — você usou await dentro de uma função normal. Adicione async à declaração da função, ou mova o await para uma função wrapper.
Paralelismo: cuidado com await sequencial
Cada await faz a função pausar até a Promise resolver. Se você precisa de 3 dados independentes, esperar um por um é desperdício.
// Sequencial: 3 viagens, uma após a outra. Total ~ 3x latência
const u = await fetch('/api/users').then(r => r.json());
const p = await fetch('/api/posts').then(r => r.json());
const t = await fetch('/api/tags').then(r => r.json());
// Paralelo: 3 viagens ao mesmo tempo. Total ~ 1x latência
const [u, p, t] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/tags').then(r => r.json()),
]);
5. fetch() — GET
A Fetch API é a interface moderna do navegador para fazer requests HTTP. Substitui XMLHttpRequest. Está disponível em todos os navegadores modernos e também no Node.js 18+.
Forma básica:
// Sem segundo argumento, fetch usa method GET por default
export async function getJSON(url) {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status} em ${url}`);
return await res.json();
}
Pontos importantes do objeto Response:
res.ok—truese status entre 200 e 299.res.status— código numérico (200, 404, 500...).res.statusText— descrição textual ("OK", "Not Found").res.headers— objetoHeaders; useres.headers.get('Content-Type').res.json(),res.text(),res.blob()— todos retornam Promise. Você precisa deawait.
Sim — esse é exatamente o padrão da nossa stack. O SSR entrega o HTML com a estrutura e o usuário vê algo imediatamente. O JS do cliente, quando carrega, faz fetch('/api/users') para popular a parte dinâmica. Isso desliga acoplamento entre layout e dados.
6. fetch() — POST com JSON
Para enviar dados ao servidor, passamos um segundo argumento ao fetch com método, headers e body:
export async function postJSON(url, payload) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) {
const erro = await res.json().catch(() => ({}));
throw new Error(erro.message ?? `HTTP ${res.status}`);
}
return await res.json();
}
1. method: 'POST' — sem isso vira GET silenciosamente, e seu controller no Express jamais será chamado.
2. 'Content-Type': 'application/json' — o middleware express.json() só parseia o corpo se este header estiver presente. Sem ele, req.body chega vazio.
3. JSON.stringify(payload) — passar o objeto cru transforma em "[object Object]" no corpo. Você precisa serializar manualmente.
Outros métodos seguem o mesmo padrão:
method: 'PUT'ou'PATCH'para atualizar.method: 'DELETE'normalmente sem body.
7. Erro de rede vs erro HTTP — o pulo do gato
Aqui está o detalhe que mais derruba quem está começando: fetch() NÃO rejeita por status 4xx ou 5xx. A Promise resolve com a Response, e o status apenas indica que algo deu errado no servidor.
| Cenário | Promise rejeita? | Por quê |
|---|---|---|
| Sem internet | SIM | Não foi possível alcançar o servidor. |
| DNS falhou | SIM | Host não existe. |
| CORS bloqueou | SIM | Resposta nem chegou ao seu código. |
| Request abortado | SIM | Cancelamento via AbortController. |
| 404 Not Found | NÃO | Servidor respondeu — ele só disse que não achou. |
| 422 Validation Error | NÃO | Servidor respondeu (com seu zod reclamando). |
| 500 Internal Error | NÃO | Servidor respondeu com falha. |
Por isso a checagem if (!res.ok) throw new Error(...) é obrigatória em qualquer wrapper de fetch:
async function getJSON(url) {
const res = await fetch(url);
if (!res.ok) {
// res.ok é true só para status 200-299
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
return await res.json();
}
8. AbortController — cancelando requests
Imagine um campo de busca que dispara um fetch a cada tecla. O usuário digita "afo", "afon", "afons", "afonso". Cinco requests viajam pela rede em ordem, mas a resposta pode chegar fora de ordem. Você acaba mostrando resultados de uma busca antiga em cima da nova — bug clássico.
A solução é AbortController: gerar um signal que pode ser usado para cancelar requests pendentes.
let controller = null;
input.addEventListener('input', async (e) => {
// Cancela request anterior (se ainda em voo)
if (controller) controller.abort();
controller = new AbortController();
try {
const res = await fetch(
`/api/search?q=${encodeURIComponent(e.target.value)}`,
{ signal: controller.signal }
);
renderResultados(await res.json());
} catch (err) {
if (err.name === 'AbortError') return; // silencioso e esperado
mostrarErro(err);
}
});
Quando usar:
- Typeahead / autocomplete — substitui o request anterior a cada tecla.
- Troca de aba ou rota — cancela o que estava carregando para a aba antiga.
- Timeout — combine com
setTimeout(() => controller.abort(), 5000).
9. Loading / Empty / Error / Success
Toda UI que depende de uma resposta assíncrona vive em quatro estados. Esquecer um deles é a fonte mais comum de UI quebrada na vida real.
Loading
Request em andamento. Mostre spinner, skeleton, ou pelo menos uma mensagem.
setLoading()
Empty
Resposta OK, mas data.length === 0. NÃO é erro — é vazio. Mensagem amigável + CTA.
"Nenhum item ainda. Criar?"
Error
Rede ou status >= 400. Mostre o erro e botão "tentar novamente".
renderError(err.message)
Success
Renderiza a lista, tabela, formulário preenchido — o caminho feliz.
renderList(data)
import { getJSON } from './api.js';
const root = document.querySelector('#lista-usuarios');
async function carregar() {
renderLoading();
try {
const { data } = await getJSON('/api/users');
if (data.length === 0) return renderEmpty();
renderList(data);
} catch (err) {
renderError(err);
}
}
function renderLoading() { root.innerHTML = '<p class="loading">Carregando...</p>'; }
function renderEmpty() { root.innerHTML = '<p>Nenhum usuário cadastrado ainda.</p>'; }
function renderError(err) {
root.innerHTML = `<p class="error">Falha: ${err.message}</p>`;
}
function renderList(users) {
root.innerHTML = users.map(u => `<li>${u.name}</li>`).join('');
}
carregar();
10. Backend dual — HTML + JSON reusando o service
O segredo de manter o código limpo é não duplicar lógica. O mesmo UserService que serve a página EJS também serve o endpoint JSON. O que muda é apenas o controller: um chama res.render, o outro chama res.json.
import { Request, Response } from 'express';
import { UserService } from '../services/UserService';
export class UserController {
constructor(private readonly service: UserService) {}
// HTML — renderiza EJS para carga inicial
index = async (req: Request, res: Response) => {
const users = await this.service.listAll();
res.render('users/index', { users });
};
// JSON — endpoint da API
apiList = async (req: Request, res: Response) => {
const users = await this.service.listAll();
res.json({ data: users });
};
}
const ctrl = new UserController(new UserService(new UserRepository(db)));
// Rotas HTML
router.get('/users', ctrl.index);
router.get('/users/:id', ctrl.show);
// Rotas JSON (mesmo controller, métodos diferentes)
router.get('/api/users', ctrl.apiList);
router.post('/api/users', ctrl.apiCreate);
Padrão de envelope JSON
Existem duas escolas. Não há certo — escolha uma e seja consistente:
| Estilo | Resposta de sucesso | Resposta de erro |
|---|---|---|
| Direto | res.json(users) → [...] |
res.status(404).json({ message: '...' }) |
| Envelope | res.json({ data: users }) |
res.status(404).json({ error: { message: '...' } }) |
11. CORS resumido
CORS (Cross-Origin Resource Sharing) é um mecanismo do navegador que bloqueia requests entre origens diferentes, a menos que o servidor diga explicitamente "tudo bem".
Origem = protocolo + host + porta. Por exemplo:
http://localhost:3000ehttp://localhost:3000→ mesma origem.http://localhost:3000ehttp://localhost:8080→ origens diferentes.https://app.x.comehttps://api.x.com→ origens diferentes.
O Express serve EJS e JSON da mesma origem (localhost:3000). O navegador aceita o fetch('/api/...') sem questionar. Use caminhos relativos.
Se em produção você separar front (app.x.com) e API (api.x.com), o navegador bloqueia. Solução: instalar o middleware cors no Express e configurar origin: 'https://app.x.com'.
12. Headers comuns
| Header | Direção | Função |
|---|---|---|
Content-Type |
Cliente → Servidor (e vice-versa) | Tipo do corpo. application/json é o nosso padrão para POST/PUT. |
Accept |
Cliente → Servidor | O que o cliente sabe ler. Servidor pode escolher formato baseado nele. |
Authorization |
Cliente → Servidor | Token bearer (Bearer xyz...) ou Basic. Não usaremos hoje — usamos sessão. |
Cache-Control |
Servidor → Cliente | Política de cache (no-store, max-age=300, ...). |
Cookie |
Cliente → Servidor | Cookies do domínio. Vão automáticos em mesma origem. |
Set-Cookie |
Servidor → Cliente | Define um cookie no browser. Atributos: HttpOnly, Secure, SameSite. |
13. Cookies de sessão e SameSite
Quando o servidor cria uma sessão (login, carrinho), ele responde com um Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax. O navegador armazena. A partir daí, todo request para o mesmo domínio anexa o cookie automaticamente — inclusive os fetch().
- HttpOnly — JS não consegue ler (proteção contra XSS).
- Secure — só envia em HTTPS.
- SameSite=Lax (default seguro) — cookie envia em GETs cross-site (links externos), mas não em POSTs cross-site (ataques CSRF).
- SameSite=Strict — só mesma origem.
- SameSite=None; Secure — cross-site permitido (usado quando frontend e API são origens diferentes).
Sessão é gerenciada pelo Express + express-session ou similar. Como front e back compartilham origem, o cookie viaja transparente em todos os fetch() — sem precisar mexer em credentials.
Se um dia usar fetch entre origens diferentes e quiser cookies, precisa de fetch(url, { credentials: 'include' }) + cors({ credentials: true }) no servidor + SameSite=None; Secure no cookie. Há atrito proposital.
14. Network tab do DevTools
F12 → Network. É onde você diagnostica 90% dos problemas de integração front/back.
- URL e método — confirma para onde foi e como.
- Status — colorido: 200 verde, 3xx azul, 4xx amarelo, 5xx vermelho.
- Headers (request e response) — útil para conferir
Content-Type,Cookie, etc. - Payload — corpo enviado.
- Response — corpo recebido (JSON pré-formatado).
- Timing — DNS, conexão, espera, download. Onde está a lentidão?
Filtre por Fetch/XHR para esconder requests de assets (imagens, CSS) e ver só as chamadas da sua API. Marque "Preserve log" para manter o histórico ao navegar.
15. Exemplo end-to-end — botão "Favoritar"
Vamos juntar todo o conhecimento da aula em um caso real: a página users/show.ejs mostra um usuário com um botão "Favoritar". Clique aciona POST /api/users/:id/favorite, atualiza o estado no servidor e a UI muda sem recarregar.
15.1 — A view EJS (carga inicial)
<article class="user-card" data-user-id="<%= user.id %>">
<h1><%= user.name %></h1>
<p><%= user.email %></p>
<button id="btn-fav" aria-pressed="<%= user.favorited %>">
<%= user.favorited ? 'Favorito' : 'Favoritar' %>
</button>
</article>
<script type="module" src="/js/favorite.js"></script>
15.2 — O JS do cliente
import { postJSON } from './api.js';
const btn = document.querySelector('#btn-fav');
const card = btn.closest('.user-card');
const id = card.dataset.userId;
btn.addEventListener('click', async () => {
btn.disabled = true;
const textoOriginal = btn.textContent;
btn.textContent = 'Salvando...';
try {
const { favorited } = await postJSON(`/api/users/${id}/favorite`, {});
btn.textContent = favorited ? 'Favorito' : 'Favoritar';
btn.setAttribute('aria-pressed', String(favorited));
} catch (err) {
btn.textContent = textoOriginal;
alert('Falha ao favoritar: ' + err.message);
} finally {
btn.disabled = false;
}
});
15.3 — O endpoint no servidor
apiToggleFavorite = async (req: Request, res: Response) => {
const id = Number(req.params.id);
if (!id) return res.status(400).json({ message: 'id inválido' });
const favorited = await this.service.toggleFavorite(id);
res.json({ favorited });
};
// router.post('/api/users/:id/favorite', ctrl.apiToggleFavorite);
SSR entrega o estado inicial. fetch + async/await consultam o back-end. try/catch/finally trata erro e estado de loading. A UI muda sem recarregar a página. Esse padrão escala para likes, comentários, exclusão, edição inline — qualquer interação da sua aplicação.
16. Intro a TCP/IP, DNS e HTTPS
Quando você escreve fetch('/api/users'), uma cascata acontece por baixo. Vamos vê-la em alto nível agora — a aula 11 abre cada caixa-preta dessa.
- DNS — quando o cliente pede
https://api.x.com/users, primeiro um servidor DNS traduzapi.x.comem IP (ex.:54.230.10.20). - TCP/IP — protocolos da camada de transporte que garantem entrega ordenada e confiável de pacotes entre cliente e servidor.
- TLS / HTTPS — uma camada de criptografia que envelopa o HTTP.
https://= HTTP rodando sobre TLS. - HTTP — o protocolo de aplicação. Um request é texto: linha de método/path, headers, linha em branco, body.
Em duas semanas vamos detalhar o handshake TCP, a negociação TLS, o que aparece em um trace de rede e por que latência geográfica importa. Por enquanto, basta ter o mapa: nome → IP → conexão → criptografia → request.
17. Checklist da aula
- ✓Sei diferenciar microtasks (Promises) de macrotasks (setTimeout) e prever a ordem de execução.
- ✓Conheço os três estados de uma Promise e os métodos
.then,.catch,.finally. - ✓Sei usar
Promise.all,Promise.allSettledePromise.race. - ✓Reconheço que
awaitsó funciona dentro de funçãoasync. - ✓Faço
fetchGET com tratamento deres.ok. - ✓Faço
fetchPOST commethod,Content-TypeeJSON.stringify. - ✓Sei explicar por que
fetchNÃO rejeita em status 4xx/5xx. - ✓Implemento cancelamento com
AbortControllere ignoroAbortError. - ✓Toda chamada na minha UI tem os 4 estados: loading, empty, error, success.
- ✓Tenho o mesmo controller respondendo HTML (res.render) e JSON (res.json), reusando o service.
- ✓Sei abrir o Network tab e diagnosticar status, headers e payload.
- ✓Tenho um helper
api.jscomgetJSONepostJSONreusáveis.
18. Referências
- MDN — Fetch API — referência oficial.
- MDN — AbortController — cancelamento de requests.
- MDN — Promise — estados, métodos, exemplos.
- MDN — async function — sintaxe e semântica.
- javascript.info — Event Loop — explicação detalhada do loop e microtasks.
- MDN — CORS — quando é problema, como resolver.
- MDN — HTTP Cookies — atributos, SameSite, segurança.
- Chrome DevTools — Network — guia oficial do painel.
- RFC 9110 — HTTP Semantics — especificação atual de status codes, métodos, headers.