📚 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 comandos npm devem ser executados de dentro da pasta web-svelte.

✅ Testado em: Windows 11 com Node.js 20+ / 22+, Vite 6 e Svelte 5 (Runes em JavaScript puro, sem TypeScript).

🚀 Projeto Lista de Tarefas (Parte 02): Cliente Web Reativo com Svelte 5 (JavaScript Puro)

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

🧭 A Filosofia do Svelte 5: Compilação Cirúrgica e Runes sem Virtual DOM

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

Por que Svelte 5 com JavaScript Puro nesta etapa?

  1. Foco Cristalino nos Fundamentos da Web: Você não precisa lidar com a sobrecarga sintática de TypeScript (interfaces, tipos genéricos, decoradores) logo após sair do Java. Todo o código usa JavaScript padrão (fetch, async/await, Array.map, Array.filter).
  2. Reatividade Explícita com Runes ($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).
  3. Eventos Padrão do DOM: No Svelte 5, não há sintaxe proprietária para eventos: usamos diretamente os atributos padrão do HTML/DOM (onclick, onsubmit, onchange, ondblclick, onkeydown, onblur).
  4. Bundle Mínimo e Inicialização Instantânea: Sem carregar um motor de diffing de árvores virtuais, o pacote final gerado pelo Vite tem poucos kilobytes e carregamento quase instantâneo.

As 3 Runes do Svelte 5 que você dominará neste projeto

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();

🏛️ 1. Visão Geral da Arquitetura & Papel no Ecossistema

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.

Arquitetura Desacoplada: Svelte SPA e Spring Boot

Comparativo Arquitetural: Thymeleaf (SSR) vs. Svelte 5 (SPA)

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

⚡ 2. Reatividade sem Virtual DOM, Ciclo Assíncrono e CORS

O ciclo de vida de uma operação na SPA Svelte percorre quatro etapas coordenadas:

Ciclo de Reatividade Svelte e API Spring Boot

  1. Interação do Usuário (Eventos DOM): O usuário envia o formulário (onsubmit), marca o checkbox (onchange), dá duplo clique no texto (ondblclick) ou clica em excluir (onclick).
  2. Reatividade Compilada (Runes): No toggle de conclusão e na exclusão, o 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.
  3. Chamada de Rede Nativa (tarefaService.js): A camada de serviço dispara um fetch() assíncrono para http://localhost:8080/api/tarefas.
  4. Confirmação ou Rollback Automático: Se a API confirmar (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.

Segurança de Navegador e o Mecanismo de CORS (Cross-Origin Resource Sharing)

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.

Conceito do Handshake CORS

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.


📐 3. Engenharia de Software & Especificação de Requisitos

3.1. Histórias de Usuário (User Stories)

3.2. Requisitos Funcionais (RF)

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}

3.3. Requisitos Não-Funcionais (RNF)

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 }.

3.4. Diagrama de Casos de Uso e Fluxo de Componentes

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

3.5. Matriz de Rastreabilidade de Requisitos (RTM)

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()

🛠️ 4. Construção da SPA Svelte Passo a Passo (Tutorial Autoguiado)

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.

💻 Pré-requisito: Verificando o Node.js e Preparando o VS Code

  1. Verifique se o Node.js e o npm estão instalados: Abra o PowerShell no Windows e execute:
    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.

  2. Extensão Recomendada no Visual Studio Code:
    • Pressione Ctrl + Shift + X no VS Code para abrir a aba de Extensões.
    • Busque por Svelte for VS Code (identificador oficial: svelte.svelte-vscode, publicado por Svelte).
    • Clique em Install. Essa extensão ativa coloração de sintaxe para arquivos .svelte, suporte às Runes do Svelte 5 ($state, $derived, $props) e autocompletar de HTML/CSS/JS dentro dos componentes.

📂 Passo 0: Criar a Pasta do Projeto e a Estrutura de Diretórios

Nossa aplicação possui uma arquitetura limpa dividida em 3 camadas dentro de src/:

🛠️ Como criar as pastas:

🌳 Mapa Completo dos 11 Arquivos que Vamos Construir (Passos 1 a 7):

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

📦 Passo 1: Configuração do Projeto, Vite e Compilador (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.

🛠️ Como criar estes 3 arquivos:

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:

  1. package.json
  2. vite.config.js
  3. svelte.config.js

🌳 Onde estes arquivos ficam localizados:

web-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/

📄 Arquivo 1 de 3: 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"
  }
}

📄 Arquivo 2 de 3: vite.config.js

import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';

// https://vite.dev/config/
export default defineConfig({
  plugins: [svelte()],
  server: {
    port: 5173,
    host: true
  }
});

📄 Arquivo 3 de 3: svelte.config.js

import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

export default {
  preprocess: vitePreprocess()
};

🚀 Instalando as Dependências:

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


🌐 Passo 2: Documento Hospedeiro HTML, Estilos Globais e Ponto de Entrada (index.html, src/app.css e src/main.js)

Toda Single Page Application (SPA) precisa de três peças de inicialização:

  1. 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.
  2. 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).
  3. 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">.

🛠️ Como criar estes 3 arquivos:

  1. Na raiz de web-svelte/ (ao lado do package.json), crie o arquivo index.html.
  2. Dentro da pasta src/, crie o arquivo app.css.
  3. Dentro da pasta src/, crie o arquivo main.js.

🌳 Onde estes arquivos ficam localizados:

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, o index.html fica sempre na raiz do projeto (web-svelte/index.html), e não dentro de src/ nem de public/.

📄 Arquivo 1 de 3: 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>

📄 Arquivo 2 de 3: 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;
}

📄 Arquivo 3 de 3: src/main.js

import { mount } from 'svelte';
import App from './App.svelte';
import './app.css';

const app = mount(App, {
  target: document.getElementById('app')
});

export default app;

🎯 Checkpoint de Verificação 2: Hospedeiro HTML, Design Tokens e Bootstrap


🔌 Passo 3: Camada de Serviço HTTP com Fetch API (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).

🛠️ Como criar o arquivo:

🌳 Onde este arquivo fica localizado:

web-svelte/
└── src/
    ├── app.css
    ├── main.js
    └── services/
        └── tarefaService.js   ← [NOVO: Camada de comunicação REST com Fetch API]

📄 Código Completo: 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}`);
    }
  }
};

🔍 Entendendo os Detalhes Técnicos deste Arquivo:

🎯 Checkpoint de Verificação 3: Serviço HTTP Desacoplado


➕ Passo 4: Componente de Cadastro de Tarefas (src/components/TarefaForm.svelte)

Agora criaremos o nosso primeiro componente Svelte! Todo arquivo .svelte pode conter três seções opcionais:

  1. <script>: lógica em JavaScript e declaração de Runes ($props, $state).
  2. Marcação HTML com ligação reativa (bind:value, {expressao}).
  3. <style>: CSS escopado automaticamente (as regras escritas aqui só afetam este componente e nunca vazam para o resto da página).

🛠️ Como criar o arquivo:

🌳 Onde este arquivo fica localizado:

web-svelte/
└── src/
    ├── services/
    │   └── tarefaService.js
    └── components/
        └── TarefaForm.svelte  ← [NOVO: Formulário de inclusão de tarefas]

📄 Código Completo: 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>

🔍 Entendendo os Detalhes Técnicos deste Componente:

🎯 Checkpoint de Verificação 4: Formulário Reativo com Prevenção de Duplo Envio


✏️ Passo 5: Componente de Item Individual e Edição In-Place (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):

🛠️ Como criar o arquivo:

🌳 Onde este arquivo fica localizado:

web-svelte/
└── src/
    ├── services/
    │   └── tarefaService.js
    └── components/
        ├── TarefaForm.svelte
        └── TarefaItem.svelte  ← [NOVO: Item individual com edição in-place]

📄 Código Completo: 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>

🔍 Entendendo os Detalhes Técnicos deste Componente:

🎯 Checkpoint de Verificação 5: Componente de Linha e Edição In-Place


📋 Passo 6: Componente de Listagem, Filtros e Contadores Derivados (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:

  1. totalConcluidas: quantas tarefas têm concluida === true.
  2. totalPendentes: quantas tarefas têm concluida === false.
  3. tarefasFiltradas: o subconjunto de tarefas que deve aparecer na tela conforme o botão de filtro selecionado ('todas', 'pendentes' ou 'concluidas').

🛠️ Como criar o arquivo:

🌳 Onde este arquivo fica localizado:

web-svelte/
└── src/
    ├── services/
    │   └── tarefaService.js
    └── components/
        ├── TarefaForm.svelte
        ├── TarefaItem.svelte
        └── TarefaList.svelte  ← [NOVO: Barra de filtros, contadores $derived e lista]

📄 Código Completo: 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>

🔍 Entendendo os Detalhes Técnicos deste Componente:

🎯 Checkpoint de Verificação 6: Lista Reativa e Estado Derivado ($derived)


🧠 Passo 7: Componente Raiz Orquestrador (src/App.svelte)

Por fim, criaremos o componente principal App.svelte dentro de src/. Ele é o orquestrador da aplicação:

🛠️ Como criar o arquivo:

🌳 Estrutura Completa após este passo:

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

📄 Código Completo: 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)


⚙️ 5. Conferindo a Configuração de CORS no Backend Spring Boot (Parte 01)

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 (5173 do Vite, 4200 do Angular, 9000 do 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"})

🧪 6. Roteiro de Homologação Integrada & Solução de Problemas

Com todos os arquivos criados e validados no build, vamos colocar o ecossistema Full Stack (Backend Java + Frontend Svelte) para conversar em tempo real!

Passo 1: Iniciar o Backend Spring Boot (Terminal 1)

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

Passo 2: Iniciar a SPA Svelte (Terminal 2)

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.

Matriz de Testes de Aceitação (Checkpoints Full Stack)

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 ✅

🩺 Solução de Problemas Comuns (Troubleshooting)

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.

🎯 7. Conclusão e Próximos Passos

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:

👉 Próximo Passo: Parte 03 — Cliente Desktop com Java Swing & Ecossistema Multi-Cliente

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!