📚 Pré-requisitos Teóricos e Práticos: Para aproveitar este projeto ao máximo, é fundamental ter concluído a Parte 01 — API REST com Spring Boot (ou estar com o backend rodando na porta
8080). Este projeto aplica na prática os fundamentos ensinados em Módulo 05: JavaScript e DOM e na Especialização Frontend: Svelte e SvelteKit.
📁 Código deste projeto: subpasta
web-svelte/. Você pode construir a aplicação do zero seguindo a Seção 4 passo a passo (recomendado: todos os 11 arquivos aparecem na ordem exata de criação, completos e 100% idênticos aos do repositório) ou abrir o código pronto e usar este guia como referência técnica. Todos os comandosnpmdevem ser executados de dentro da pastaweb-svelte.✅ Testado em: Windows 11 com Node.js 20+ / 22+, Vite 6 e Svelte 5 (Runes em JavaScript puro, sem TypeScript).
Bem-vindo à Parte 02 da Trilha Prática de Aplicações Full Stack!
Na Parte 01, você construiu a fundação da nossa aplicação: uma API REST com Spring Boot, banco de dados relacional H2 em memória, validações visuais com a extensão YARC e uma interface clássica com Thymeleaf (Server-Side Rendering, onde cada clique ou envio de formulário recarrega a página HTML inteira a partir do servidor Java).
Agora daremos um salto arquitetural: construiremos uma Single Page Application (SPA) moderna, ultraleve e reativa com Svelte 5 em JavaScript puro (ES2022+), executando via Vite na porta 5173 e consumindo de forma assíncrona a mesmíssima API Spring Boot na porta 8080.
Trilha Prática de Aplicações Full Stack (Série Lista de Tarefas):
├── Parte 01: API REST com Spring Boot + YARC + Thymeleaf (SSR) [Concluído]
├── Parte 02: Cliente Web Desacoplado com Svelte 5 (JavaScript Puro) [ESTE PROJETO]
├── Parte 03: Cliente Desktop Clássico com Java Swing (Multi-Cliente)
├── Parte 04: Cliente Mobile Nativo Clássico com Android (Java + XML Views)
├── Parte 05: Arquitetura, Automação e Docker + PostgreSQL 16
├── Parte 06: Web ➔ Mobile Híbrido com Vue.js 3 ➔ Quasar Framework
├── Parte 07: Web ➔ Mobile Híbrido com React ➔ React Native (Expo)
├── Parte 08: Web ➔ Mobile Híbrido com Angular ➔ Ionic (Capacitor)
├── Parte 09: Evolução Arquitetural Desktop: De Swing para JavaFX Moderno
└── Parte 10: Evolução Arquitetural Mobile: De Java/XML para Kotlin + Jetpack Compose
Ao contrário de frameworks tradicionais que enviam uma biblioteca pesada de runtime e um motor de reconciliação de Virtual DOM (VDOM) para dentro do navegador do usuário, o Svelte adota uma abordagem radicalmente diferente: ele é um compilador.
Paradigma de Virtual DOM (React / Vue):
Código Declarativo ──> Runtime no Navegador ──> Virtual DOM Diffing ──> Mutações no DOM Real
Paradigma Compilado (Svelte 5):
Código Declarativo (.svelte) ──> Compilador (Build Time) ──> JS Imperativo Cirúrgico ──> Atualização Direta no DOM
fetch, async/await, Array.map, Array.filter).$state, $derived, $props): O Svelte 5 introduziu as Runes — primitivas reativas universais que deixam explícito o que é estado mutável ($state), o que é valor calculado automaticamente ($derived) e o que um componente recebe do pai ($props).onclick, onsubmit, onchange, ondblclick, onkeydown, onblur).| Rune | Função no Svelte 5 | Onde usamos no projeto | Exemplo real no código |
|---|---|---|---|
$state(valorInicial) |
Declara uma variável reativa. Quando ela muda, o DOM ligado a ela atualiza sozinho. | App.svelte, TarefaForm.svelte, TarefaList.svelte, TarefaItem.svelte |
let tarefas = $state([]); |
$derived(expressao) |
Calcula um valor derivado que recalcula automaticamente sempre que um $state lido por ele mudar. |
TarefaList.svelte (contadores e filtro ativo) |
let totalPendentes = $derived(tarefas.filter(t => !t.concluida).length); |
$props() |
Recebe propriedades e funções de callback passadas pelo componente pai via desestruturação JS. | TarefaForm.svelte, TarefaList.svelte, TarefaItem.svelte |
let { tarefa, onAlternar, onExcluir, onAtualizar } = $props(); |
O cliente Web Svelte opera de maneira 100% desacoplada do backend. Ele é servido ao navegador como um conjunto de artefatos estáticos (index.html, app.css, módulos .js e .svelte compilados pelo Vite) e conversa com a Fonte Única da Verdade (SSOT) por meio de requisições HTTP assíncronas em formato JSON.
| Dimensão | Parte 01 — Thymeleaf (SSR) | Parte 02 — Svelte 5 (SPA) |
|---|---|---|
| Onde o HTML é montado? | No servidor Java (TarefaWebController + motor Thymeleaf) |
No navegador do usuário (JavaScript compilado pelo Svelte) |
| Ações do Usuário (Criar/Concluir) | Recarrega a página inteira (Full Page Reload) via redirect:/tarefas |
Atualiza cirurgicamente apenas o elemento <li class="tarefa-item"> afetado |
| Formato de Transporte | application/x-www-form-urlencoded (Formulários HTML) |
application/json via fetch() assíncrono |
| Acoplamento | Alto (templates HTML ficam dentro do .jar do Spring Boot) |
Zero (a API REST /api/tarefas atende Web, Desktop e Mobile sem mudar nada) |
| Experiência de Uso (UX) | Página web clássica | Aplicativo fluido com atualização otimista e edição in-place |
O ciclo de vida de uma operação na SPA Svelte percorre quatro etapas coordenadas:
onsubmit), marca o checkbox (onchange), dá duplo clique no texto (ondblclick) ou clica em excluir (onclick).App.svelte aplica Atualização Otimista: salva uma cópia de segurança (const tarefasOriginais = [...tarefas]) e já atualiza o estado $state imediatamente para que a interface responda em zero milissegundos.tarefaService.js): A camada de serviço dispara um fetch() assíncrono para http://localhost:8080/api/tarefas.200 OK, 201 Created ou 204 No Content), a operação está concluída. Se o servidor estiver desligado ou retornar erro HTTP, o bloco catch restaura tarefas = tarefasOriginais e exibe um banner de alerta amigável no topo da tela.Como o servidor de desenvolvimento do Vite roda na porta 5173 (http://localhost:5173) e o Tomcat do Spring Boot roda na porta 8080 (http://localhost:8080), o navegador enxerga duas origens diferentes (portas distintas caracterizam origens distintas) e aplica a política de segurança Same-Origin Policy.
Antes de enviar requisições como POST, PUT ou DELETE com corpo JSON (Content-Type: application/json), o navegador dispara automaticamente uma requisição de pré-voo (Preflight) com o verbo HTTP OPTIONS. Na Parte 01, já anotamos o TarefaRestController com @CrossOrigin(origins = "*"), o que instrui o Spring Boot a devolver o cabeçalho Access-Control-Allow-Origin autorizando o nosso frontend Svelte.
+ Adicionar) para que ela apareça imediatamente no topo da lista sem recarregar a página.Enter (ou ao clicar fora) e cancelando com Escape.| ID | Nome do Requisito | Descrição Detalhada | Método & Rota na API |
|---|---|---|---|
| RF-S01 | Listagem Inicial e Filtros | Buscar a lista de tarefas ao montar o componente (onMount), exibir spinner de carregamento e filtrar localmente via $derived. |
GET /api/tarefas |
| RF-S02 | Inclusão Assíncrona | Validar entrada (ignorar texto em branco com .trim()), desabilitar botão durante o envio, disparar POST e inserir a tarefa retornada no início do array. |
POST /api/tarefas |
| RF-S03 | Toggle Otimista de Conclusão | Inverter concluida otimisticamente na UI, sincronizar o objeto atualizado via PUT e reverter o array original em caso de exceção. |
PUT /api/tarefas/{id} |
| RF-S04 | Edição In-Place | Abrir campo de texto focado ao dar duplo clique ou clicar em ✏️; salvar via PUT apenas se o texto limpo mudou e não está vazio; cancelar com Escape. |
PUT /api/tarefas/{id} |
| RF-S05 | Exclusão com Confirmação | Solicitar confirmação (confirm()), remover otimisticamente da lista, enviar DELETE e restaurar o estado caso a API retorne erro. |
DELETE /api/tarefas/{id} |
| ID | Categoria | Critério Técnico |
|---|---|---|
| RNF-S01 | Interoperabilidade CORS | A API Spring Boot na porta 8080 deve autorizar requisições vindas de http://localhost:5173. |
| RNF-S02 | JavaScript Puro (Vanilla ES2022) | O projeto deve ser 100% JavaScript padrão (.js e .svelte), sem exigir compilador TypeScript. |
| RNF-S03 | Svelte 5 Runes & Zero VDOM | Gerenciamento de estado feito exclusivamente com as Runes do Svelte 5 ($state, $derived, $props). |
| RNF-S04 | Zero Dependências de Rede | Comunicação HTTP realizada com a fetch() nativa do navegador, sem Axios ou bibliotecas externas. |
| RNF-S05 | Contrato Canônico (SSOT) | Respeitar estritamente o esquema JSON da Parte 01: { "id": 1, "descricao": "...", "concluida": false }. |
flowchart LR
User(["👤 Usuário no Navegador (Porta 5173)"])
subgraph SvelteApp ["🧡 Single Page Application (Svelte 5 + JS)"]
direction TB
UC01["UC01: Adicionar Tarefa (TarefaForm.svelte)"]
UC02["UC02: Listar & Filtrar com $derived (TarefaList.svelte)"]
UC03["UC03: Alternar Conclusão Otimista (TarefaItem.svelte)"]
UC04["UC04: Editar Descrição In-Place (TarefaItem.svelte)"]
UC05["UC05: Excluir Tarefa com Rollback (TarefaItem.svelte)"]
end
subgraph SpringAPI ["⚙️ API REST Spring Boot (Porta 8080)"]
direction TB
E1["POST /api/tarefas (201)"]
E2["GET /api/tarefas (200)"]
E3["PUT /api/tarefas/{id} (200)"]
E4["DELETE /api/tarefas/{id} (204)"]
end
User --> UC01
User --> UC02
User --> UC03
User --> UC04
User --> UC05
UC01 -.->|"JSON {descricao, concluida: false}"| E1
UC02 -.->|"Array JSON"| E2
UC03 -.->|"JSON {descricao, concluida}"| E3
UC04 -.->|"JSON {descricao, concluida}"| E3
UC05 -.->|"HTTP DELETE"| E4
| História de Usuário | Requisito Funcional | Requisito Não-Funcional | Caso de Uso | Arquivo(s) e Função Responsável |
|---|---|---|---|---|
| US-S01 (Inclusão Ágil) | RF-S02 | RNF-S02, RNF-S03, RNF-S04 | UC01 | TarefaForm.svelte (handleSubmit) → App.svelte (adicionarTarefa) → tarefaService.criarTarefa() |
| US-S04 (Filtro e Contadores) | RF-S01 | RNF-S02, RNF-S03 | UC02 | App.svelte (carregarTarefas) → TarefaList.svelte ($derived: tarefasFiltradas, totalPendentes, totalConcluidas) |
| US-S02 (Alternância Status) | RF-S03 | RNF-S01, RNF-S04, RNF-S05 | UC03 | TarefaItem.svelte (onchange) → App.svelte (alternarStatus) → tarefaService.alternarStatusTarefa() |
| US-S03 (Edição In-Place) | RF-S04 | RNF-S03, RNF-S05 | UC04 | TarefaItem.svelte (iniciarEdicao, salvarEdicao, cancelarEdicao) → App.svelte (atualizarTarefa) → tarefaService.atualizarTarefa() |
| US-S05 (Exclusão Segura) | RF-S05 | RNF-S01, RNF-S04 | UC05 | TarefaItem.svelte (onclick) → App.svelte (excluirTarefa) → tarefaService.excluirTarefa() |
Neste guia autoguiado, você construirá todos os 11 arquivos que compõem a aplicação web-svelte/. Não omitimos nenhum arquivo: seguindo os passos de 0 a 7, sua aplicação funcionará de primeira.
node -v
npm -v
💡 Recomendamos o Node.js 20 LTS ou 22 LTS (que já inclui o
npm 10+). Caso o comando não seja reconhecido, baixe o instalador LTS oficial em nodejs.org.
Ctrl + Shift + X no VS Code para abrir a aba de Extensões.Svelte for VS Code (identificador oficial: svelte.svelte-vscode, publicado por Svelte)..svelte, suporte às Runes do Svelte 5 ($state, $derived, $props) e autocompletar de HTML/CSS/JS dentro dos componentes.Nossa aplicação possui uma arquitetura limpa dividida em 3 camadas dentro de src/:
src/services/ $\rightarrow$ Comunicação HTTP com a API Spring Boot.src/components/ $\rightarrow$ Componentes visuais reutilizáveis (TarefaForm, TarefaItem, TarefaList).src/ (raiz de código) $\rightarrow$ Folha de estilos global (app.css), componente orquestrador (App.svelte) e inicializador (main.js).catalogo_projetos\fullstack_tarefas_02_spring_svelte) e execute:
# Cria a pasta raiz do cliente web e entra nela
mkdir web-svelte
cd web-svelte
# Cria a pasta src e as subpastas de serviços e componentes
mkdir src, src\services, src\components
web-svelte e abra-a no VS Code (File > Open Folder…).src.src $\rightarrow$ New Folder $\rightarrow$ digite services.src $\rightarrow$ New Folder $\rightarrow$ digite components.web-svelte/
├── package.json ← [Passo 1] Dependências (Svelte 5 + Vite 6) e scripts npm
├── vite.config.js ← [Passo 1] Servidor Vite na porta 5173
├── svelte.config.js ← [Passo 1] Pré-processador do compilador Svelte
├── index.html ← [Passo 2] Página HTML hospedeira (<div id="app">)
└── src/
├── app.css ← [Passo 2] Estilos globais e variáveis CSS (:root)
├── main.js ← [Passo 2] Ponto de entrada JS que monta o App.svelte
├── services/
│ └── tarefaService.js ← [Passo 3] Cliente HTTP com fetch() nativo (:8080)
├── components/
│ ├── TarefaForm.svelte ← [Passo 4] Formulário reativo de inclusão
│ ├── TarefaItem.svelte ← [Passo 5] Linha de tarefa com checkbox e edição in-place
│ └── TarefaList.svelte ← [Passo 6] Barra de filtros, contadores $derived e lista
└── App.svelte ← [Passo 7] Orquestrador de estado global e rollback otimista
package.json, vite.config.js e svelte.config.js)Vamos criar os 3 arquivos de configuração na raiz da pasta web-svelte/. Eles informam ao npm quais pacotes baixar e como o Vite deve compilar arquivos .svelte.
No VS Code, clique com o botão direito na raiz da pasta web-svelte (fora de src/), escolha New File e crie um por um:
package.jsonvite.config.jssvelte.config.jsweb-svelte/
├── package.json ← [NOVO: Manifesto de dependências e scripts]
├── vite.config.js ← [NOVO: Configuração do servidor Vite na porta 5173]
├── svelte.config.js ← [NOVO: Pré-processamento do compilador Svelte]
└── src/
package.json{
"name": "web-svelte",
"private": true,
"version": "1.0.0",
"type": "module",
"description": "Cliente Web Desacoplado em Svelte com JavaScript Puro consumindo a API Spring Boot SSOT",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"svelte": "^5.0.0",
"vite": "^6.0.0"
}
}
"type": "module": habilita módulos ES nativos (import / export) em todo o projeto."devDependencies": repare que tudo fica em devDependencies! Como o Svelte é um compilador, ele transforma seus componentes em JavaScript puro durante o build.vite.config.jsimport { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
// https://vite.dev/config/
export default defineConfig({
plugins: [svelte()],
server: {
port: 5173,
host: true
}
});
plugins: [svelte()]: conecta o compilador do Svelte 5 ao pipeline do Vite com Hot Module Replacement (HMR) instantâneo.port: 5173: fixa a porta padrão do cliente web para bater com as regras de CORS.svelte.config.jsimport { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess()
};
vitePreprocess(): permite que o compilador Svelte processe blocos <style> modernos e recursos do Vite dentro dos arquivos .svelte.Com os 3 arquivos salvos, abra o terminal integrado do VS Code (Ctrl + ') dentro da pasta web-svelte e execute:
npm install
🎯 Checkpoint de Verificação 1: Fundação Vite + Svelte 5 Instalada
- O que você acabou de implementar: O manifesto
package.jsone as configurações do bundler Vite e do compilador Svelte.- Como validar agora: Confirme que o comando
npm installterminou comadded ... packagese que a pastanode_modules/e o arquivopackage-lock.jsonapareceram dentro deweb-svelte/.- Reflexão de Engenharia de Software (RNF-S02, RNF-S03): Diferente de projetos pesados que instalam centenas de megabytes de compiladores de tipos e runtimes, nossa árvore de dependências contém exclusivamente o Vite e o compilador Svelte 5.
index.html, src/app.css e src/main.js)Toda Single Page Application (SPA) precisa de três peças de inicialização:
index.html (na raiz web-svelte/): A única página HTML da aplicação. Ela contém uma <div id="app"></div> vazia onde o Svelte vai montar a interface.src/app.css (dentro de src/): Nossa folha de estilos global com variáveis CSS (Design Tokens como --primary-color, --success-color, --border-color) e classes utilitárias (.container, .card, .btn, .alert).src/main.js (dentro de src/): O script inicial que importa o app.css e usa a função mount() do Svelte 5 para injetar o componente App.svelte dentro da <div id="app">.web-svelte/ (ao lado do package.json), crie o arquivo index.html.src/, crie o arquivo app.css.src/, crie o arquivo main.js.web-svelte/
├── index.html ← [NOVO: HTML hospedeiro na raiz do projeto]
├── package.json
├── svelte.config.js
├── vite.config.js
└── src/
├── app.css ← [NOVO: Variáveis CSS (:root) e estilos globais]
└── main.js ← [NOVO: Ponto de entrada que monta a SPA]
⚠️ Atenção ao local do
index.html: No Vite, oindex.htmlfica sempre na raiz do projeto (web-svelte/index.html), e não dentro desrc/nem depublic/.
index.html<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tarefas Full Stack — Svelte + JavaScript</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
src/app.css:root {
--primary-color: #ff3e00; /* Laranja característico do Svelte */
--primary-hover: #e03600;
--success-color: #10b981;
--danger-color: #ef4444;
--bg-color: #f8fafc;
--card-bg: #ffffff;
--text-main: #0f172a;
--text-muted: #64748b;
--border-color: #e2e8f0;
--radius: 10px;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
line-height: 1.5;
padding: 2rem 1rem;
}
.container {
max-width: 680px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 2rem;
}
header h1 {
font-size: 2rem;
font-weight: 700;
color: var(--text-main);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
header p {
color: var(--text-muted);
margin-top: 0.25rem;
font-size: 0.95rem;
}
.card {
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow);
border: 1px solid var(--border-color);
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.6rem 1.2rem;
border-radius: 8px;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
border: none;
transition: all 0.15s ease-in-out;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--primary-hover);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.alert {
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.alert-error {
background-color: #fee2e2;
color: #991b1b;
border: 1px solid #fecaca;
}
.alert-success {
background-color: #dcfce7;
color: #166534;
border: 1px solid #bbf7d0;
}
src/main.jsimport { mount } from 'svelte';
import App from './App.svelte';
import './app.css';
const app = mount(App, {
target: document.getElementById('app')
});
export default app;
new App({ target: ... }). No Svelte 5, componentes não são mais classes instanciadas com new, mas funções montadas explicitamente com mount(App, { target }) importado de 'svelte'.🎯 Checkpoint de Verificação 2: Hospedeiro HTML, Design Tokens e Bootstrap
- O que você acabou de implementar: O documento
index.html, o sistema de Design Tokens emsrc/app.csse o montadorsrc/main.js.- Como validar agora: Verifique se
<div id="app"></div>noindex.htmltem exatamente o mesmo ID buscado pordocument.getElementById('app')nosrc/main.js. (Ainda não rodenpm run dev, pois omain.jsimporta./App.svelte, que criaremos no Passo 7).- Reflexão de Engenharia de Software: Concentrar variáveis de cor e espaçamento em
:rootnoapp.csspermite que cada componente.sveltetenha seu próprio<style>isolado (escopado pelo compilador), mas mantendo identidade visual unificada.
src/services/tarefaService.js)Seguindo o princípio de Separação de Responsabilidades (SoC), nenhum componente visual .svelte deve montar URLs ou cabeçalhos HTTP diretamente. Criaremos o módulo tarefaService.js dentro de src/services/ para encapsular todas as chamadas fetch() à API Spring Boot (http://localhost:8080/api/tarefas).
src/services $\rightarrow$ New File $\rightarrow$ digite tarefaService.js.web-svelte/
└── src/
├── app.css
├── main.js
└── services/
└── tarefaService.js ← [NOVO: Camada de comunicação REST com Fetch API]
src/services/tarefaService.js// Serviço em JavaScript Puro utilizando a API Fetch nativa do navegador
// Comunica-se diretamente com a Fonte Única da Verdade (SSOT): API Spring Boot na porta 8080
const API_URL = 'http://localhost:8080/api/tarefas';
export const tarefaService = {
/**
* Consulta a lista completa de tarefas cadastradas
* @returns {Promise<Array<{id: number, descricao: string, concluida: boolean}>>}
*/
async listarTarefas() {
const resposta = await fetch(API_URL);
if (!resposta.ok) {
throw new Error(`Erro ao consultar API: HTTP ${resposta.status}`);
}
return await resposta.json();
},
/**
* Cadastra uma nova tarefa
* @param {string} descricao
* @returns {Promise<{id: number, descricao: string, concluida: boolean}>}
*/
async criarTarefa(descricao) {
const resposta = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
descricao: descricao.trim(),
concluida: false
})
});
if (!resposta.ok) {
throw new Error(`Erro ao criar tarefa: HTTP ${resposta.status}`);
}
return await resposta.json();
},
/**
* Atualiza a descrição e/ou status de uma tarefa existente
* @param {number} id
* @param {string} descricao
* @param {boolean} concluida
* @returns {Promise<{id: number, descricao: string, concluida: boolean}>}
*/
async atualizarTarefa(id, descricao, concluida) {
const resposta = await fetch(`${API_URL}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
descricao: descricao.trim(),
concluida
})
});
if (!resposta.ok) {
throw new Error(`Erro ao atualizar tarefa: HTTP ${resposta.status}`);
}
return await resposta.json();
},
/**
* Alterna o status de conclusão sincronizando o registro completo via PUT
* @param {number} id
* @param {string} descricao
* @param {boolean} concluida
* @returns {Promise<{id: number, descricao: string, concluida: boolean}>}
*/
async alternarStatusTarefa(id, descricao, concluida) {
return this.atualizarTarefa(id, descricao, concluida);
},
/**
* Remove permanentemente uma tarefa por ID
* @param {number} id
* @returns {Promise<void>}
*/
async excluirTarefa(id) {
const resposta = await fetch(`${API_URL}/${id}`, {
method: 'DELETE'
});
if (!resposta.ok && resposta.status !== 204) {
throw new Error(`Erro ao excluir tarefa: HTTP ${resposta.status}`);
}
}
};
if (!resposta.ok)? A função fetch() do navegador só lança exceção sozinha quando há falha física de rede (ex.: servidor desligado). Se o servidor responder 404 Not Found ou 500 Internal Server Error, a Promise do fetch() resolve normalmente! Por isso checamos resposta.ok (que é true para status HTTP entre 200 e 299) e lançamos throw new Error(...) para ativar o rollback no componente.excluirTarefa não chama resposta.json()? Na Parte 01, o método DELETE /api/tarefas/{id} do Spring Boot retorna 204 No Content (corpo vazio). Chamar .json() numa resposta sem corpo lançaria um SyntaxError: Unexpected end of JSON input.alternarStatusTarefa envia descricao e concluida via PUT? Porque o endpoint PUT /api/tarefas/{id} da Parte 01 atualiza a entidade Tarefa completa (setDescricao e setConcluida). Passando a descrição atual junto com o novo booleano concluida, garantimos total fidelidade ao contrato REST da Parte 01.🎯 Checkpoint de Verificação 3: Serviço HTTP Desacoplado
- O que você acabou de implementar: O objeto exportado
tarefaServicecom as 5 operações assíncronas (listarTarefas,criarTarefa,atualizarTarefa,alternarStatusTarefaeexcluirTarefa).- Como validar agora: Confirme que o arquivo está salvo em
src/services/tarefaService.jse começa comexport const tarefaService = {.- Reflexão de Engenharia de Software (RNF-S04, RNF-S05): Se no futuro a URL da API mudar de
localhost:8080para um domínio em nuvem, você alterará uma única constante (API_URL) neste arquivo, sem tocar em nenhum componente visual.
src/components/TarefaForm.svelte)Agora criaremos o nosso primeiro componente Svelte! Todo arquivo .svelte pode conter três seções opcionais:
<script>: lógica em JavaScript e declaração de Runes ($props, $state).bind:value, {expressao}).<style>: CSS escopado automaticamente (as regras escritas aqui só afetam este componente e nunca vazam para o resto da página).src/components $\rightarrow$ New File $\rightarrow$ digite TarefaForm.svelte (com T e F maiúsculos).web-svelte/
└── src/
├── services/
│ └── tarefaService.js
└── components/
└── TarefaForm.svelte ← [NOVO: Formulário de inclusão de tarefas]
src/components/TarefaForm.svelte<script>
let { onAdicionar } = $props();
let descricao = $state('');
let enviando = $state(false);
async function handleSubmit(event) {
event.preventDefault();
const texto = descricao.trim();
if (!texto) return;
enviando = true;
try {
await onAdicionar(texto);
descricao = '';
} finally {
enviando = false;
}
}
</script>
<form class="form-tarefa card" onsubmit={handleSubmit}>
<div class="input-group">
<input
type="text"
placeholder="O que você precisa fazer hoje?"
bind:value={descricao}
disabled={enviando}
/>
<button type="submit" class="btn btn-primary" disabled={enviando || !descricao.trim()}>
{enviando ? 'Adicionando...' : '+ Adicionar'}
</button>
</div>
</form>
<style>
.form-tarefa {
margin-bottom: 1.5rem;
padding: 1.25rem;
}
.input-group {
display: flex;
gap: 0.75rem;
}
input[type='text'] {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 1rem;
color: var(--text-main);
background: #fdfdfd;
outline: none;
transition: border-color 0.15s ease-in-out;
}
input[type='text']:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(255, 62, 0, 0.15);
}
</style>
let { onAdicionar } = $props();: recebe do componente pai (App.svelte) a função assíncrona que salvará a tarefa na API.bind:value={descricao}: cria um two-way data binding (ligação bidirecional). Quando o usuário digita no <input>, a variável reativa descricao atualiza na hora; quando fazemos descricao = '' após salvar, o campo de texto limpa sozinho!disabled={enviando || !descricao.trim()}: o botão + Adicionar já nasce desabilitado enquanto o campo estiver vazio ou só com espaços em branco, e muda seu rótulo para 'Adicionando...' enquanto aguarda a resposta HTTP.🎯 Checkpoint de Verificação 4: Formulário Reativo com Prevenção de Duplo Envio
- O que você acabou de implementar: O componente
TarefaForm.svelteusando$props(),$state()ebind:value.- Como validar agora: Verifique se o arquivo está salvo em
src/components/TarefaForm.sveltesem erros sublinhados pela extensão do Svelte.- Reflexão de Engenharia de Software (US-S01, RF-S02): Usar
try ... finally { enviando = false; }garante que o campo e o botão nunca fiquem travados em estado desabilitado mesmo se a rede cair durante oPOST.
src/components/TarefaItem.svelte)Cada linha da nossa lista de tarefas é representada pelo componente TarefaItem.svelte. Ele possui dois modos visuais controlados pelo estado booleano let editando = $state(false):
editando === false): Exibe o checkbox de conclusão, o texto da tarefa (que aceita duplo clique via ondblclick={iniciarEdicao}), a tag #id e os botões de ação ✏️ (Editar) e 🗑️ (Excluir).editando === true): Substitui o texto por um <input class="input-edicao"> focado automaticamente, que salva ao pressionar Enter ou perder o foco (onblur={salvarEdicao}) e cancela ao pressionar Escape.src/components $\rightarrow$ New File $\rightarrow$ digite TarefaItem.svelte.web-svelte/
└── src/
├── services/
│ └── tarefaService.js
└── components/
├── TarefaForm.svelte
└── TarefaItem.svelte ← [NOVO: Item individual com edição in-place]
src/components/TarefaItem.svelte<script>
let { tarefa, onAlternar, onExcluir, onAtualizar } = $props();
let editando = $state(false);
let textoEditado = $state('');
function iniciarEdicao() {
textoEditado = tarefa.descricao;
editando = true;
}
function cancelarEdicao() {
editando = false;
}
async function salvarEdicao() {
const texto = textoEditado.trim();
if (!texto) return;
if (texto !== tarefa.descricao) {
await onAtualizar(tarefa.id, texto, tarefa.concluida);
}
editando = false;
}
function handleKeyDown(event) {
if (event.key === 'Enter') {
salvarEdicao();
} else if (event.key === 'Escape') {
cancelarEdicao();
}
}
</script>
<li class="tarefa-item" class:concluida={tarefa.concluida}>
<div class="content-col">
<input
type="checkbox"
checked={tarefa.concluida}
onchange={() => onAlternar(tarefa.id, !tarefa.concluida)}
class="checkbox-status"
title={tarefa.concluida ? 'Marcar como pendente' : 'Marcar como concluída'}
/>
{#if editando}
<input
type="text"
class="input-edicao"
bind:value={textoEditado}
onkeydown={handleKeyDown}
onblur={salvarEdicao}
autofocus
/>
{:else}
<span
class="descricao"
ondblclick={iniciarEdicao}
title="Dê duplo clique para editar"
>
{tarefa.descricao}
</span>
<span class="id-tag">#{tarefa.id}</span>
{/if}
</div>
<div class="actions-col">
{#if !editando}
<button class="btn-action btn-edit" onclick={iniciarEdicao} title="Editar">
✏️
</button>
<button class="btn-action btn-delete" onclick={() => onExcluir(tarefa.id)} title="Excluir">
🗑️
</button>
{/if}
</div>
</li>
<style>
.tarefa-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.9rem 1.1rem;
background: #ffffff;
border: 1px solid var(--border-color);
border-radius: 8px;
margin-bottom: 0.6rem;
transition: all 0.15s ease-in-out;
}
.tarefa-item:hover {
border-color: #cbd5e1;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
}
.tarefa-item.concluida {
background: #f8fafc;
border-left: 4px solid var(--success-color);
}
.tarefa-item:not(.concluida) {
border-left: 4px solid var(--primary-color);
}
.content-col {
display: flex;
align-items: center;
gap: 0.85rem;
flex: 1;
}
.checkbox-status {
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
accent-color: var(--success-color);
}
.descricao {
font-size: 1rem;
color: var(--text-main);
cursor: pointer;
word-break: break-word;
}
.tarefa-item.concluida .descricao {
text-decoration: line-through;
color: var(--text-muted);
opacity: 0.8;
}
.id-tag {
font-size: 0.75rem;
color: #94a3b8;
background: #f1f5f9;
padding: 0.15rem 0.4rem;
border-radius: 4px;
margin-left: 0.25rem;
}
.input-edicao {
flex: 1;
padding: 0.4rem 0.6rem;
font-size: 1rem;
border: 1px solid var(--primary-color);
border-radius: 6px;
outline: none;
}
.actions-col {
display: flex;
gap: 0.35rem;
}
.btn-action {
background: transparent;
border: none;
font-size: 1rem;
cursor: pointer;
padding: 0.3rem 0.5rem;
border-radius: 6px;
transition: background-color 0.12s;
}
.btn-action:hover {
background: #f1f5f9;
}
.btn-delete:hover {
background: #fee2e2;
}
</style>
class:concluida={tarefa.concluida}: O Svelte adiciona ou remove automaticamente a classe CSS .concluida no elemento <li> conforme o booleano tarefa.concluida seja true ou false. Quando concluida é true, a borda esquerda fica verde (var(--success-color)) e o texto recebe text-decoration: line-through.salvarEdicao(), verificamos if (texto !== tarefa.descricao) antes de chamar onAtualizar(). Se o usuário abriu a edição e apertou Enter sem mudar o texto, nenhuma requisição PUT é gasta na rede!🎯 Checkpoint de Verificação 5: Componente de Linha e Edição In-Place
- O que você acabou de implementar: O componente
TarefaItem.sveltecom suporte a alternância de status, edição por duplo clique ou botão ✏️ e exclusão.- Como validar agora: Confirme que
TarefaItem.svelteestá salvo dentro desrc/components/.- Reflexão de Engenharia de Software (US-S02, US-S03): Ao manter
editandoetextoEditadocomo$statelocais dentro deTarefaItem.svelte, cada linha gerencia seu próprio modo de edição de forma isolada, sem poluir o estado global da aplicação.
src/components/TarefaList.svelte)Agora criaremos o componente TarefaList.svelte, que recebe o array tarefas do componente pai e utiliza a rune $derived() do Svelte 5 para calcular em tempo real:
totalConcluidas: quantas tarefas têm concluida === true.totalPendentes: quantas tarefas têm concluida === false.tarefasFiltradas: o subconjunto de tarefas que deve aparecer na tela conforme o botão de filtro selecionado ('todas', 'pendentes' ou 'concluidas').src/components $\rightarrow$ New File $\rightarrow$ digite TarefaList.svelte.web-svelte/
└── src/
├── services/
│ └── tarefaService.js
└── components/
├── TarefaForm.svelte
├── TarefaItem.svelte
└── TarefaList.svelte ← [NOVO: Barra de filtros, contadores $derived e lista]
src/components/TarefaList.svelte<script>
import TarefaItem from './TarefaItem.svelte';
let { tarefas, onAlternar, onExcluir, onAtualizar } = $props();
let filtro = $state('todas'); // 'todas' | 'pendentes' | 'concluidas'
let totalConcluidas = $derived(tarefas.filter(t => t.concluida).length);
let totalPendentes = $derived(tarefas.filter(t => !t.concluida).length);
let tarefasFiltradas = $derived(
tarefas.filter(t => {
if (filtro === 'pendentes') return !t.concluida;
if (filtro === 'concluidas') return t.concluida;
return true;
})
);
</script>
<div class="tarefas-container card">
<!-- Barra de Filtros e Contadores -->
<div class="toolbar-filtros">
<div class="botoes-filtro">
<button
class="filtro-btn"
class:ativo={filtro === 'todas'}
onclick={() => (filtro = 'todas')}
>
Todas <span class="badge">{tarefas.length}</span>
</button>
<button
class="filtro-btn"
class:ativo={filtro === 'pendentes'}
onclick={() => (filtro = 'pendentes')}
>
Pendentes <span class="badge">{totalPendentes}</span>
</button>
<button
class="filtro-btn"
class:ativo={filtro === 'concluidas'}
onclick={() => (filtro = 'concluidas')}
>
Concluídas <span class="badge">{totalConcluidas}</span>
</button>
</div>
</div>
<!-- Lista de Itens -->
{#if tarefasFiltradas.length > 0}
<ul class="lista-tarefas">
{#each tarefasFiltradas as tarefa (tarefa.id)}
<TarefaItem {tarefa} {onAlternar} {onExcluir} {onAtualizar} />
{/each}
</ul>
{:else}
<div class="empty-state">
<p class="empty-icon">📝</p>
<h3>Nenhuma tarefa encontrada</h3>
<p class="empty-text">
{#if filtro === 'todas'}
Adicione uma nova tarefa acima para começar a organizar seu dia!
{:else}
Não há tarefas nesta categoria de filtro.
{/if}
</p>
</div>
{/if}
</div>
<style>
.tarefas-container {
padding: 1.5rem;
}
.toolbar-filtros {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border-color);
}
.botoes-filtro {
display: flex;
gap: 0.5rem;
}
.filtro-btn {
background: transparent;
border: 1px solid var(--border-color);
padding: 0.4rem 0.8rem;
border-radius: 6px;
font-size: 0.88rem;
font-weight: 500;
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s ease-in-out;
display: flex;
align-items: center;
gap: 0.4rem;
}
.filtro-btn:hover {
border-color: #cbd5e1;
color: var(--text-main);
}
.filtro-btn.ativo {
background: var(--text-main);
color: white;
border-color: var(--text-main);
}
.filtro-btn.ativo .badge {
background: rgba(255, 255, 255, 0.25);
color: white;
}
.badge {
background: #e2e8f0;
color: var(--text-main);
font-size: 0.75rem;
padding: 0.1rem 0.45rem;
border-radius: 12px;
}
.lista-tarefas {
list-style: none;
}
.empty-state {
text-align: center;
padding: 2.5rem 1rem;
color: var(--text-muted);
}
.empty-icon {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.empty-state h3 {
color: var(--text-main);
font-size: 1.1rem;
margin-bottom: 0.25rem;
}
.empty-text {
font-size: 0.9rem;
}
</style>
(tarefa.id) no {#each tarefasFiltradas as tarefa (tarefa.id)}? Esse parêntese final é a chave de identidade (Keyed Each Block) do Svelte. Ele informa ao compilador qual propriedade identifica unicamente cada item do banco de dados. Assim, quando você exclui ou filtra uma tarefa, o Svelte remove ou oculta cirurgicamente apenas o nó <li> daquele id, sem recriar os demais elementos da lista!{tarefa} {onAlternar} {onExcluir} {onAtualizar}: Em Svelte, quando o nome da prop é igual ao nome da variável, escrever {tarefa} equivale a escrever tarefa={tarefa}.🎯 Checkpoint de Verificação 6: Lista Reativa e Estado Derivado (
$derived)
- O que você acabou de implementar: O componente
TarefaList.svelte, completando todos os 3 componentes da pastasrc/components/.- Como validar agora: Abra a pasta
src/components/no Explorer do VS Code e confirme que estão lá os três arquivos:TarefaForm.svelte,TarefaItem.svelteeTarefaList.svelte.- Reflexão de Engenharia de Software (US-S04, RF-S01): Com
$derived, nunca corremos o risco de esquecer de atualizar um contador após criar ou excluir uma tarefa: os contadores são funções puras derivadas do arraytarefas.
src/App.svelte)Por fim, criaremos o componente principal App.svelte dentro de src/. Ele é o orquestrador da aplicação:
onMount), chama carregarTarefas() para buscar os dados iniciais da API Spring Boot.let tarefas = $state([]), o indicador let carregando = $state(true) e a mensagem let erro = $state('').alternarStatus e excluirTarefa.src (ao lado de main.js e app.css) $\rightarrow$ New File $\rightarrow$ digite App.svelte (com A maiúsculo).web-svelte/
├── index.html
├── package.json
├── svelte.config.js
├── vite.config.js
└── src/
├── app.css
├── main.js
├── App.svelte ← [NOVO: Componente Orquestrador Raiz]
├── services/
│ └── tarefaService.js
└── components/
├── TarefaForm.svelte
├── TarefaItem.svelte
└── TarefaList.svelte
src/App.svelte<script>
import { onMount } from 'svelte';
import { tarefaService } from './services/tarefaService.js';
import TarefaForm from './components/TarefaForm.svelte';
import TarefaList from './components/TarefaList.svelte';
let tarefas = $state([]);
let carregando = $state(true);
let erro = $state('');
onMount(async () => {
await carregarTarefas();
});
async function carregarTarefas() {
carregando = true;
erro = '';
try {
tarefas = await tarefaService.listarTarefas();
} catch (e) {
console.error(e);
erro = 'Não foi possível conectar à API Spring Boot (porta 8080). Verifique se o backend está rodando.';
} finally {
carregando = false;
}
}
async function adicionarTarefa(descricao) {
erro = '';
try {
const nova = await tarefaService.criarTarefa(descricao);
tarefas = [nova, ...tarefas];
} catch (e) {
console.error(e);
erro = 'Falha ao salvar nova tarefa na API.';
}
}
async function alternarStatus(id, novoStatus) {
const tarefaAtual = tarefas.find(t => t.id === id);
if (!tarefaAtual) return;
erro = '';
// Atualização otimista na interface
const tarefasOriginais = [...tarefas];
tarefas = tarefas.map(t => (t.id === id ? { ...t, concluida: novoStatus } : t));
try {
await tarefaService.alternarStatusTarefa(id, tarefaAtual.descricao, novoStatus);
} catch (e) {
console.error(e);
// Reverte em caso de falha
tarefas = tarefasOriginais;
erro = 'Falha ao sincronizar alteração de status com o backend.';
}
}
async function atualizarTarefa(id, novaDescricao, status) {
erro = '';
try {
const atualizada = await tarefaService.atualizarTarefa(id, novaDescricao, status);
tarefas = tarefas.map(t => (t.id === id ? atualizada : t));
} catch (e) {
console.error(e);
erro = 'Falha ao atualizar descrição da tarefa na API.';
}
}
async function excluirTarefa(id) {
if (!confirm('Deseja realmente remover esta tarefa?')) return;
erro = '';
const tarefasOriginais = [...tarefas];
tarefas = tarefas.filter(t => t.id !== id);
try {
await tarefaService.excluirTarefa(id);
} catch (e) {
console.error(e);
tarefas = tarefasOriginais;
erro = 'Falha ao excluir tarefa na API.';
}
}
</script>
<div class="container">
<header>
<h1>🧡 Lista de Tarefas (Svelte)</h1>
<p>Cliente Web Reativo com JavaScript Puro · Consumindo API Spring Boot (:8080)</p>
</header>
{#if erro}
<div class="alert alert-error">
⚠️ {erro}
</div>
{/if}
<main>
<TarefaForm onAdicionar={adicionarTarefa} />
{#if carregando}
<div class="loading-box card">
<div class="spinner"></div>
<p>Carregando tarefas do servidor...</p>
</div>
{:else}
<TarefaList
{tarefas}
onAlternar={alternarStatus}
onExcluir={excluirTarefa}
onAtualizar={atualizarTarefa}
/>
{/if}
</main>
</div>
<style>
.loading-box {
text-align: center;
padding: 2.5rem;
color: var(--text-muted);
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid #e2e8f0;
border-top-color: var(--primary-color);
border-radius: 50%;
margin: 0 auto 1rem auto;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
🎯 Checkpoint de Verificação 7: Compilação Completa da SPA (Teste de Build)
- O que você acabou de implementar: O componente orquestrador
App.sveltee a integração de todos os 11 arquivos do projeto.- Como validar agora: No terminal dentro da pasta
web-svelte, teste a compilação de produção do Svelte + Vite executando:npm run buildVocê deverá ver a saída
✓ built in ...msgerando os arquivos otimizados emdist/sem nenhum erro ou alerta de importação!
Se você construiu a Parte 01 seguindo o nosso guia, o seu controlador TarefaRestController.java (no pacote br.com.curso.listadetarefas.api.tarefa) já possui a anotação @CrossOrigin(origins = "*") na linha 11:
package br.com.curso.listadetarefas.api.tarefa;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/tarefas")
@CrossOrigin(origins = "*") // Já autoriza o Vite (http://localhost:5173) e demais clientes!
public class TarefaRestController {
// Endpoints GET, POST, PUT e DELETE da Parte 01
}
💡 Boas Práticas em Produção: Durante o desenvolvimento local,
@CrossOrigin(origins = "*")é excelente porque permite testar clientes em diferentes portas (5173do Vite,4200do Angular,9000do Quasar). Em um ambiente corporativo de produção, recomenda-se restringir explicitamente as origens confiáveis:@CrossOrigin(origins = {"http://localhost:5173", "http://localhost:4200", "http://localhost:3000"})
Com todos os arquivos criados e validados no build, vamos colocar o ecossistema Full Stack (Backend Java + Frontend Svelte) para conversar em tempo real!
Abra um terminal na pasta api-spring da Parte 01 e inicie a API na porta 8080:
# No diretório catalogo_projetos/fullstack_tarefas_01_spring_api/api-spring
.\mvnw.cmd spring-boot:run
Em outro terminal (ou no navegador), confirme que a API está respondendo:
curl.exe -i http://localhost:8080/api/tarefas
Abra um segundo terminal na pasta web-svelte da Parte 02 e inicie o servidor de desenvolvimento do Vite:
# No diretório catalogo_projetos/fullstack_tarefas_02_spring_svelte/web-svelte
npm run dev
O Vite exibirá no terminal:
VITE v6.x.x ready in 300 ms
➜ Local: http://localhost:5173/
Abra http://localhost:5173/ no seu navegador.
Execute os 6 testes abaixo na ordem indicada para homologar todos os requisitos (RF-S01 a RF-S05):
| Checkpoint | Ação Realizada no Navegador (:5173) |
Resultado Esperado na UI e na API (:8080) |
Status |
|---|---|---|---|
| CP-01 | Abrir http://localhost:5173 com a API rodando |
O spinner some e a lista exibe as tarefas do banco H2 (ou o Empty State "Nenhuma tarefa encontrada"). |
PASS ✅ |
| CP-02 | Digitar "Dominar Svelte 5 com Runes" e pressionar Enter |
A tarefa surge no topo da lista com borda laranja e tag #1; o contador Todas e Pendentes sobe para 1. |
PASS ✅ |
| CP-03 | Marcar o checkbox da tarefa #1 |
A borda esquerda fica verde, o texto fica tachado na hora e o contador Concluídas sobe para 1. |
PASS ✅ |
| CP-04 | Dar duplo clique no texto da tarefa (ou clicar em ✏️), alterar para "Dominar Svelte 5 e Fetch API" e apertar Enter |
O input fecha, o novo texto permanece salvo e, ao recarregar a página (F5), tanto o texto editado quanto o status concluído continuam gravados no H2! |
PASS ✅ |
| CP-05 | Clicar nos botões de filtro Pendentes e Concluídas | A lista filtra instantaneamente sem disparar novas requisições de rede (cálculo reativo local via $derived). |
PASS ✅ |
| CP-06 | Clicar no botão 🗑️ e confirmar na caixa de diálogo | A tarefa desaparece imediatamente e DELETE /api/tarefas/1 retorna 204 No Content. |
PASS ✅ |
| Sintoma / Mensagem de Erro | Causa Provável | Como Resolver |
|---|---|---|
Failed to resolve import "./components/TarefaList.svelte" no terminal do Vite |
Algum dos arquivos dos Passos 3 a 6 não foi criado ou está com nome/maiúsculas diferentes, ou foi salvo fora da subpasta correta (src/components/ ou src/services/). |
Confira a árvore de arquivos no Passo 7 e verifique se os nomes respeitam maiúsculas e minúsculas: TarefaForm.svelte, TarefaItem.svelte, TarefaList.svelte em src/components/ e tarefaService.js em src/services/. |
| Página em branco no navegador sem erro no terminal | O arquivo index.html não tem <div id="app"></div> ou <script type="module" src="/src/main.js"></script>, ou index.html foi colocado dentro de src/. |
Mova index.html para a raiz web-svelte/index.html (ao lado de package.json), conforme o Passo 2. |
Banner vermelho: "⚠️ Não foi possível conectar à API Spring Boot (porta 8080)" |
O backend da Parte 01 não está em execução na porta 8080. |
Inicie o backend Spring Boot no Terminal 1 (.\mvnw.cmd spring-boot:run) e recarregue http://localhost:5173. |
| Componentes aparecem sem cores ou sem bordas arredondadas | Falta a linha import './app.css'; no src/main.js ou o arquivo src/app.css está vazio. |
Verifique o Passo 2 e confirme que src/app.css foi criado e importado em src/main.js. |
Parabéns! Você construiu do zero uma Single Page Application (SPA) completa, reativa e desacoplada utilizando Svelte 5 (Runes) e JavaScript puro, integrando-a à sua API REST Spring Boot.
Até aqui na nossa série Lista de Tarefas, você já domina:
$state, $derived, $props), atualização otimista com rollback e consumo HTTP nativo com fetch().Na Parte 03, provaremos na prática o poder da nossa Fonte Única da Verdade (SSOT): construiremos um aplicativo Desktop nativo em Java Swing que consome simultaneamente a mesma API na porta 8080 — tudo o que você criar no Svelte aparecerá no aplicativo Desktop e vice-versa!