Fase 4: Conectando o Frontend ao Backend¶
Objetivo: Substituir o estado local "de mentira" do frontend por chamadas reais à nossa API — e, no caminho, encontrar (de propósito!) e corrigir o famoso erro de CORS.
Passo 4.1: Adicionar o core.async ao deps.edn¶
Para conversar com a API, o frontend fará requisições assíncronas (fetch). Em ClojureScript, a forma idiomática de lidar com assincronia é a biblioteca core.async (com os blocos go e o operador <p!, que "espera" uma Promise do JavaScript).
Ação: Abra o deps.edn e adicione o core.async ao bloco :deps:
;; --- Dependências do Frontend ---
thheller/shadow-cljs {:mvn/version "2.28.23"}
reagent/reagent {:mvn/version "2.0.0"}
org.clojure/core.async {:mvn/version "1.6.681"} ;; <- ADICIONE ESTA LINHA
Warning
Lembre-se da regra de ouro: mudou o deps.edn, reinicie o que estiver rodando. Se o npx shadow-cljs watch app estiver ativo, pare-o (Ctrl+C) e suba de novo — só assim ele enxerga a nova dependência.
Passo 4.2: O Primeiro fetch — e o Erro CORS (Intencional)¶
Objetivo: vamos tentar buscar a lista de todos do backend. Vamos esperar que isso falhe, para ver por que o CORS existe.
Ação 1: Adicionar os requires de assincronia
No ns do src/todo/frontend/core.cljs:
(ns todo.frontend.core
(:require [reagent.core :as r]
[reagent.dom.client :as rdom]
[clojure.string :as str]
;; --- ADICIONE ESTAS DUAS LINHAS ---
[cljs.core.async :refer [go]]
[cljs.core.async.interop :refer-macros [<p!]]))
Ação 2: Adicionar as funções da API
Abaixo do defonce app-state, adicione o api-url e as funções fetch-json e get-todos:
;; --- A URL base da nossa API (o backend, porta 3000) ---
(def api-url "http://localhost:3000/api")
;; Função auxiliar: faz o fetch e converte a resposta JSON
;; em dados Clojure (mapas com keywords).
(defn fetch-json [url options]
(-> (js/fetch url (clj->js options))
(.then (fn [response]
(when-not (.-ok response)
(throw (js/Error. (str "HTTP error: " (.-status response)))))
(.json response)))
;; Converte o objeto JS em dados Clojure com chaves keyword
(.then #(js->clj % :keywordize-keys true))))
;; Busca todos os "todos" da API
(defn get-todos []
(swap! app-state assoc :loading true :error nil)
(go
(try
(let [response (<p! (fetch-json (str api-url "/todos") {:method "GET"}))]
(swap! app-state assoc :todos (:todos response) :loading false))
(catch js/Error e
(swap! app-state assoc :error (.-message e) :loading false)))))
Tip
Como ler o go + <p!: o bloco (go ...) cria um "processo" assíncrono. Dentro dele, (<p! promise) significa "pause aqui até a Promise resolver e me dê o valor". É o equivalente do async/await do JavaScript, em ClojureScript.
Ação 3: Chamar get-todos na inicialização
Encontre a função init no final do arquivo e adicione a chamada:
(defn ^:export init []
(println "Frontend inicializado...")
(let [root (rdom/create-root (js/document.getElementById "app"))]
(.render root (r/as-element [app])))
;; --- ADICIONE ESTA LINHA ---
;; Ao carregar a página, busca os todos da API
(get-todos))
Ação 4: Teste (A Falha Intencional)
Agora, sim, vamos rodar ambos os servidores:
- Terminal 1 (Backend):
(Deve dizer Servidor iniciado na porta 3000. Este servidor ainda não sabe nada sobre CORS.)
- Terminal 2 (Frontend):
(Espere o Build completed.)
- Navegador: abra
http://localhost:8000.
Resultado Esperado: a página carrega, mas a lista de todos não aparece.
A "Lição" (O "Aha!"):
- Abra o Console do Desenvolvedor (
F12, aba Console). - Você verá um erro vermelho bem claro:
Access to fetch at 'http://localhost:3000/api/todos' from origin
'http://localhost:8000' has been blocked by CORS policy...
O que aprendemos¶
Você acabou de descobrir o CORS (Cross-Origin Resource Sharing). O navegador, por segurança, impediu que o localhost:8000 (o frontend) fizesse uma requisição para o localhost:3000 (o backend), porque eles são "origens" diferentes (portas diferentes contam como origens diferentes!).
Importante: quem bloqueia é o navegador, não o servidor. É por isso que o curl funcionava — ele não é um navegador e não aplica a política de CORS. A correção, porém, é feita no servidor: é ele quem precisa declarar "eu confio na origem localhost:8000".
Passo 4.3: Corrigir o CORS (no Backend)¶
Ação 1: Adicionar a dependência
Abra o deps.edn e adicione o ring-cors ao bloco :deps (junto às dependências do backend):
;; --- Dependências do Backend (API REST) ---
ring/ring-core {:mvn/version "1.12.2"}
ring/ring-jetty-adapter {:mvn/version "1.12.2"}
ring/ring-json {:mvn/version "0.5.1"}
metosin/reitit-ring {:mvn/version "0.7.0"}
ring-cors/ring-cors {:mvn/version "0.1.13"} ;; <- ADICIONE ESTA LINHA
Warning
Atenção ao nome: é ring-cors/ring-cors (grupo e artefato iguais). Escrever ring/ring-cors fará o download falhar.
Ação 2: Adicionar o middleware ao core.clj
Abra src/todo/backend/core.clj:
- Requeira o
wrap-corsnons:
(ns todo.backend.core
(:require [ring.adapter.jetty :as jetty]
[reitit.ring :as ring]
[ring.middleware.json :refer [wrap-json-response wrap-json-body]]
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
;; --- ADICIONE ESTA LINHA ---
[ring.middleware.cors :refer [wrap-cors]]
[todo.backend.handler :as handler])
(:gen-class))
- Adicione o
wrap-corscomo o PRIMEIRO middleware da "linha de montagem" no(def app ...):
(def app
(ring/ring-handler
app-routes
(ring/create-default-handler)
{:middleware [;; --- ADICIONE ESTE VETOR (o primeiro da lista!) ---
[wrap-cors
;; Em quais origens o backend confia:
:access-control-allow-origin [#"http://localhost:8000"]
;; Quais métodos HTTP são permitidos:
:access-control-allow-methods [:get :post :put :delete]
;; Quais headers o frontend pode enviar:
;; (necessário para o POST com Content-Type: application/json)
:access-control-allow-headers ["Content-Type"]]
;; O resto dos middlewares (como antes)...
wrap-json-response
[wrap-json-body {:keywords? true}]
wrap-params
wrap-keyword-params]}))
Tip
Por que o wrap-cors precisa ser o primeiro? Além das requisições normais, o navegador envia uma requisição extra de "sondagem" chamada preflight: um OPTIONS perguntando "posso fazer um POST com o header Content-Type para você?". Nós não temos nenhuma rota OPTIONS — quem responde a essa pergunta é o próprio wrap-cors. Sendo o primeiro (o mais "externo") da linha de montagem, ele intercepta o preflight antes de o roteador procurar (e não achar) uma rota.
E é por isso que declaramos :access-control-allow-headers ["Content-Type"]: um GET simples não dispara preflight, mas o nosso POST com JSON dispara — e o navegador só o libera se o servidor disser explicitamente que aceita esse header. Sem essa linha, você cairia na situação mais confusa possível: o GET funciona, mas o POST continua bloqueado por CORS.
Ação 3: Teste (A Correção)
- Terminal 1 (Backend): pare (
Ctrl+C) e reinicie (clj -M:run) — crucial para baixar oring-cors. - Terminal 2 (Frontend): pode deixar rodando.
- Navegador: volte a
http://localhost:8000e recarregue (F5).
Resultado Esperado: o erro de CORS no console (F12) desapareceu! A página ainda estará vazia (não há todos no banco em memória), mas sem erros. Seu frontend agora pode "falar" com o backend.
Passo 4.4: Conectar a Criação (POST)¶
O GET funciona. Agora vamos fazer o botão "Adicionar" parar de usar a função local (adicionar-todo-local) e usar a API de verdade.
Ação 1: Adicionar a função create-todo
No src/todo/frontend/core.cljs, logo abaixo da função get-todos, adicione:
;; --- Cria um "todo" via API (POST) ---
(defn create-todo [todo-data]
(swap! app-state assoc :loading true :error nil)
(go
(try
(<p! (fetch-json (str api-url "/todos")
{:method "POST"
:headers {"Content-Type" "application/json"}
;; Converte o mapa Clojure em uma string JSON
:body (js/JSON.stringify (clj->js todo-data))}))
;; Se o POST funcionou, recarregamos a lista
(get-todos)
(catch js/Error e
(swap! app-state assoc :error (.-message e) :loading false)))))
Ação 2: Modificar o todo-form
Agora temos duas funções de "adicionar": a antiga adicionar-todo-local (de mentira) e a nova create-todo (da API). Vamos ligar o formulário na nova.
Mude esta versão:
(defn todo-form []
[:div.todo-input
[:input
{:type "text"
:placeholder "O que precisa ser feito?"
:value (:input-text @app-state)
:on-change #(swap! app-state assoc :input-text (-> % .-target .-value))}]
[:button
{:on-click adicionar-todo-local} ;; <-- MUDE AQUI
"Adicionar (Local)"]]) ;; <-- E AQUI
Para esta versão:
(defn todo-form []
[:div.todo-input
[:input
{:type "text"
:placeholder "O que precisa ser feito?"
:value (:input-text @app-state)
:on-change #(swap! app-state assoc :input-text (-> % .-target .-value))}]
[:button
{:on-click (fn []
(create-todo {:title (:input-text @app-state)})
(swap! app-state assoc :input-text ""))} ;; Limpa o input
"Adicionar"]])
Ação 3: Limpar o código
Duas pequenas faxinas para o código não "mentir":
- Delete a função
adicionar-todo-local— ela não é mais usada. (Como consequência, a chave:next-iddoapp-statetambém ficou sem uso; pode removê-la dodefonce, deixando{:input-text "" :todos []}.) - No componente
app, atualize o texto, que ainda dizia que os dados somem no F5:
Mude:
[:h1 "Todo App (Somente Frontend)"]
[:p "Isto é 100% local. Recarregue (F5) para ver os dados sumirem."]
Para:
Tip
Depois de deletar o defonce antigo com :next-id, o hot-reload pode manter o valor antigo na memória (é justamente o que o defonce faz!). Se algo parecer estranho, um simples F5 recarrega tudo do zero.
Passo 4.5: Teste Final (Full-Stack)¶
Ação 1: confira que os dois servidores estão de pé:
- Terminal 1:
clj -M:run→Servidor iniciado na porta 3000(com a correção do CORS). - Terminal 2:
npx shadow-cljs watch app→Build completed.
Ação 2: Teste no navegador
- Abra a URL do frontend:
http://localhost:8000. - Confira o console (F12): sem erros de CORS. Lista vazia.
- Digite um todo (ex: "Testar a API completa") e clique em "Adicionar".
O que acontece agora (a mágica):
- O
core.cljschamacreate-todo(oPOSTpara a porta 3000); - O navegador dispara antes um preflight
OPTIONS, que owrap-corsresponde; - O backend cria o todo no
atom; - O
create-todochamaget-todos(oGET); - O
@app-stateé atualizado com a nova lista; - O Reagent redesenha a UI → seu todo aparece na lista!
Ação 3: O "Aha!" (a persistência da API)
Faça o teste que falhou na Fase 3: recarregue a página (F5).
- O
initroda de novo e chamaget-todos; - O backend (que não foi reiniciado) ainda tem o todo no seu
atome o devolve; - Resultado: o todo continua lá!
A aplicação agora é full-stack: a UI está desacoplada dos dados, e os dados sobrevivem ao recarregamento da página (por enquanto, na memória do backend).
Passo 4.6: Git Checkpoint¶
Ação 1: pare os servidores e verifique:
Você deve ver: deps.edn (core.async + ring-cors), src/todo/backend/core.clj (wrap-cors) e src/todo/frontend/core.cljs (funções de API + limpeza).
Ação 2: prepare e salve:
Fim da Fase 4! 🏁
Temos uma aplicação full-stack funcional, com o histórico salvo no Git.
Mas ela ainda tem uma grande limitação: se você parar e reiniciar o servidor de backend (clj -M:run), os todos desaparecem — eles vivem em um atom, que é memória volátil. Faça o teste, se quiser: pare o backend, suba de novo, dê F5… lista vazia. 😢
A próxima fase resolve isso trocando o db.clj por um banco de dados real.