📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Matriz de 20 Capítulos de Aplicações Full Stack (este módulo). Recomendado revisar antes de começar.
Crie um diretório raiz para o projeto e, dentro dele, as subpastas para o backend e o frontend.
mkdir projeto-listatarefas_spring_vue_cors
cd projeto-listatarefas_spring_vue_cors
mkdir backend
mkdir frontend
—`
br.com.tarefasapiapibr.com.tarefas.apiSpring Web: Para criar APIs REST.Spring Data JPA: Para persistência de dados.H2 Database: Banco de dados em memória para desenvolvimento.Lombok: Para reduzir código boilerplate (getters, setters, construtores).Dev tools: Hot Reload..zip para a pasta backend.Inicie sua aplicação Spring Boot.
mvn spring-boot:run
Este é um exemplo comum onde o Controller acessa o Repository diretamente, misturando responsabilidades.
src/main/java/br/com/tarefas/api/Tarefa.java
package br.com.tarefas.api;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
@Entity
@Data
public class Tarefa {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String titulo;
private boolean concluida;
}
src/main/java/br/com/tarefas/api/TarefaRepository.java
package br.com.tarefas.api;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TarefaRepository extends JpaRepository<Tarefa, Long> {
}
src/main/java/br/com/tarefas/api/TarefaController.java (Incorreto)
package br.com.tarefas.api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/tarefas")
public class TarefaController {
@Autowired
private TarefaRepository tarefaRepository; // Injeção direta do Repository
@GetMapping
public List<Tarefa> listar() {
return tarefaRepository.findAll(); // Lógica de busca no controller
}
@PostMapping
public Tarefa criar(@RequestBody Tarefa tarefa) {
return tarefaRepository.save(tarefa); // Lógica de persistência no controller
}
}
Inicie sua aplicação Spring Boot.
mvn spring-boot:run
Separamos as responsabilidades em camadas: Controller (apresentação), Service (regras de negócio) e Repository (acesso a dados).
a. Camada de Serviço (Service)
Crie um novo pacote br.com.tarefas.api.service.
src/main/java/br/com/tarefas/api/service/TarefaService.java
package br.com.tarefas.api.service;
import br.com.tarefas.api.Tarefa;
import br.com.tarefas.api.TarefaRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service // Anotação que define a classe como um serviço
public class TarefaService {
@Autowired
private TarefaRepository tarefaRepository;
public List<Tarefa> listarTodas() {
// A lógica de negócio reside aqui.
// Ex: validações, orquestração de chamadas, etc.
return tarefaRepository.findAll();
}
public Tarefa criarTarefa(Tarefa tarefa) {
// Validação de entrada
if (tarefa.getTitulo() == null || tarefa.getTitulo().isBlank()) {
throw new IllegalArgumentException("O título da tarefa é obrigatório.");
}
return tarefaRepository.save(tarefa);
}
public void deletarTarefa(Long id) {
if (!tarefaRepository.existsById(id)) {
throw new RuntimeException("Tarefa não encontrada com o id: " + id); // Exceção mais específica seria melhor
}
tarefaRepository.deleteById(id);
}
}
b. Controller Refatorado
O Controller agora delega a lógica para o TarefaService.
src/main/java/br/com/tarefas/api/controller/TarefaController.java (Correto)
package br.com.tarefas.api.controller; // Pacote corrigido
import br.com.tarefas.api.Tarefa;
import br.com.tarefas.api.service.TarefaService; // Importa o serviço
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/tarefas") // Prefixo /api para rotas de dados
public class TarefaController {
@Autowired
private TarefaService tarefaService; // Injeta o Serviço, não o Repositório
@GetMapping
public ResponseEntity<List<Tarefa>> listar() {
List<Tarefa> tarefas = tarefaService.listarTodas();
return ResponseEntity.ok(tarefas);
}
@PostMapping
public ResponseEntity<Tarefa> criar(@RequestBody Tarefa tarefa) {
Tarefa novaTarefa = tarefaService.criarTarefa(tarefa);
return new ResponseEntity<>(novaTarefa, HttpStatus.CREATED);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletar(@PathVariable Long id) {
tarefaService.deletarTarefa(id);
return ResponseEntity.noContent().build();
}
}
O frontend (ex: http://localhost:5173) tentará acessar o backend (ex: http://localhost:8080). O navegador bloqueará essa requisição por padrão por segurança. O CORS informa ao navegador que o acesso é permitido.
Método 1: Anotação @CrossOrigin (Rápido, menos flexível)
Adicione a anotação diretamente na classe do Controller. Útil para testes rápidos.
// Em TarefaController.java
@CrossOrigin(origins = "http://localhost:5173") // Permite requisições desta origem
@RestController
@RequestMapping("/api/tarefas")
public class TarefaController {
// ...
}
Método 2: Configuração Global (Recomendado, centralizado e flexível)
Crie uma classe de configuração.
src/main/java/br/com/tarefas/api/config/WebConfig.java
package br.com.tarefas.api.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // Aplica a política para todas as rotas sob /api/
.allowedOrigins("http://localhost:5173") // Origem do frontend
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // Métodos HTTP permitidos
.allowedHeaders("*") // Permite todos os cabeçalhos
.allowCredentials(true);
}
}
Inicie sua aplicação Spring Boot.
mvn spring-boot:run
Esta abordagem centraliza a configuração de CORS para toda a aplicação, tornando-a mais fácil de manter.
—`
Abra o terminal (PowerShell, CMD ou Bash) e execute:
node -v
npm -v
Se ambos mostrarem versões (ex: v20.x.x e 10.x.x), o Node.js já está instalado.
—`
Execute o instalador .msi e:
Teste novamente:
node -v
npm -v
Use o NodeSource, que mantém pacotes oficiais e atualizados:
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
Depois, confirme a instalação:
node -v
npm -v
Com Homebrew instalado, basta rodar:
brew install node@lts
ou, se preferir a versão mais recente LTS:
brew install node
Verifique:
node -v
npm -v
—`
Para criar e gerenciar projetos Vue 2 ou Vue 3 com interface de linha de comando:
npm install -g @vue/cli
Verifique:
vue --version
—`
A forma moderna de criar projetos Vue 3:
—`
Navegue até a pasta listatarefas_spring_vue_cors e execute o comando abaixo no terminal.
# Na pasta /listatarefas_spring_vue_cors
npm create vue@latest
Siga as instruções:
Com base na sua tela, siga estas seleções para manter o foco nos objetivos de arquitetura e boas práticas para este projeto específico.
Use as setas (↑/↓) para navegar, a barra de espaço para selecionar/desmarcar e Enter para confirmar.
Project name: app-tarefas (Pressione Enter)
Add TypeScript?: No
Add JSX Support?: No
<template>...) é mais idiomática e clara para a maioria dos casos de uso. JSX não é necessário e adicionaria uma etapa de transpilação extra sem um benefício claro para este projeto.Add Vue Router for Single Page Application development?: No
Add Pinia for state management?: No
App.vue), tornando o uso de uma biblioteca externa um excesso de engenharia (over-engineering).Add Vitest for Unit Testing?: No
Add an End-to-End Testing Solution?: No
Add ESLint for code quality?: Yes (Pressione espaço para selecionar)
Add Prettier for code formatting?: Yes (Pressione espaço para selecionar)
Sua tela de seleção deve ficar assim antes de pressionar Enter para confirmar:
◆ Select features to include in your project:
│ ◻ TypeScript
│ ◻ JSX Support
│ ◻ Router (SPA development)
│ ◻ Pinia (state management)
│ ◻ Vitest (unit testing)
│ ◻ End-to-End Testing
│ ◉ ESLint (error prevention)
│ ◉ Prettier (code formatting)
Após pressionar Enter, o terminal exibirá as seguintes instruções. Execute-as em ordem.
Navegue para o diretório do projeto:
cd frontend
Instale as dependências do projeto:
Este comando lê o arquivo
package.jsone baixa todas as bibliotecas necessárias (Vue, ESLint, Prettier, etc.) para a pastanode_modules.
npm install
2.1 Formate o código do projeto:
Este comando executa o Prettier (ou outra ferramenta configurada) para padronizar a formatação do código.
npm run format
Inicie o servidor de desenvolvimento:
Este comando compila a aplicação e a serve localmente, geralmente em uma porta como
http://localhost:5173. Ele também observa as alterações nos arquivos e recarrega a página automaticamente.
npm run dev
Acesse a pasta do projeto e instale as dependências e o axios para requisições HTTP.
No Vue.js, o axios é uma biblioteca JavaScript usada para fazer requisições HTTP (como GET, POST, PUT, DELETE) entre o frontend (Vue) e o backend (ex: Spring Boot, Node.js, etc.).
👉 Em resumo, o axios é o “mensageiro” que permite que o Vue.js busque dados de uma API ou envie informações para ela.
—`
axios faz no Vue.js| Função | Descrição |
|---|---|
| 🔹 Requisições HTTP | Envia e recebe dados da API (ex: /api/tarefas) |
| 🔹 Integração com Backend | Permite comunicação entre Vue.js e servidores como Spring Boot |
| 🔹 Suporte a Promises | Trabalha de forma assíncrona (async/await) |
| 🔹 Interceptores | Permite adicionar tokens JWT, logs ou tratamento de erros antes de enviar/receber requisições |
| 🔹 Configuração Global | Define URL base, cabeçalhos e tempo limite para todas as requisições |
cd frontend
npm install
npm install axios
Lógica de API misturada diretamente com o componente visual.
src/App.vue (Incorreto)
<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";
const tarefas = ref([]);
const novaTarefaTitulo = ref("");
async function carregarTarefas() {
// Chamada de API hardcoded no componente
const response = await axios.get("http://localhost:8080/api/tarefas");
tarefas.value = response.data;
}
async function adicionarTarefa() {
if (!novaTarefaTitulo.value) return;
// Lógica de post diretamente no componente
const response = await axios.post("http://localhost:8080/api/tarefas", {
titulo: novaTarefaTitulo.value,
concluida: false,
});
tarefas.value.push(response.data);
novaTarefaTitulo.value = "";
}
onMounted(carregarTarefas);
</script>
<template>
<h1>Lista de Tarefas</h1>
<input
v-model="novaTarefaTitulo"
@keyup.enter="adicionarTarefa"
placeholder="Nova tarefa..."
/>
<ul>
<li v-for="tarefa in tarefas" :key="tarefa.id">{ { tarefa.titulo } }</li>
</ul>
</template>
Isolamos toda a lógica de comunicação com a API em um módulo dedicado.
a. Camada de Serviço (API Service)
Crie a pasta src/services e o arquivo tarefaService.js.
src/services/tarefaService.js
import axios from "axios";
// Instância do Axios com baseURL centralizada
const apiClient = axios.create({
baseURL: "http://localhost:8080/api", // URL base da API
headers: {
"Content-Type": "application/json",
},
});
// Exporta funções específicas para cada endpoint
export default {
getTarefas() {
return apiClient.get("/tarefas");
},
addTarefa(tarefa) {
return apiClient.post("/tarefas", tarefa);
},
deleteTarefa(id) {
return apiClient.delete(`/tarefas/${id}`);
},
};
b. Componente Refatorado
O componente agora importa e usa o serviço, focando apenas na lógica da UI e no estado.
src/App.vue (Correto)
<script setup>
import { ref, onMounted } from "vue";
// Importa o serviço de API, não o axios diretamente
import tarefaService from "./services/tarefaService";
const tarefas = ref([]);
const novaTarefaTitulo = ref("");
const erro = ref(null);
async function carregarTarefas() {
try {
erro.value = null;
const response = await tarefaService.getTarefas();
tarefas.value = response.data;
} catch (e) {
erro.value = "Falha ao carregar tarefas.";
console.error(e);
}
}
async function adicionarTarefa() {
if (!novaTarefaTitulo.value.trim()) return;
const novaTarefa = {
titulo: novaTarefaTitulo.value,
concluida: false,
};
try {
erro.value = null;
const response = await tarefaService.addTarefa(novaTarefa);
tarefas.value.push(response.data);
novaTarefaTitulo.value = "";
} catch (e) {
erro.value = "Falha ao adicionar tarefa.";
console.error(e);
}
}
async function removerTarefa(id) {
try {
erro.value = null;
await tarefaService.deleteTarefa(id);
tarefas.value = tarefas.value.filter((t) => t.id !== id);
} catch (e) {
erro.value = "Falha ao remover tarefa.";
console.error(e);
}
}
onMounted(carregarTarefas);
</script>
<template>
<main>
<h1>Lista de Tarefas</h1>
<div class="form-container">
<input
v-model="novaTarefaTitulo"
@keyup.enter="adicionarTarefa"
placeholder="Adicionar uma nova tarefa"
/>
<button @click="adicionarTarefa">Adicionar</button>
</div>
<p v-if="erro" class="erro">{ { erro } }</p>
<ul>
<li v-for="tarefa in tarefas" :key="tarefa.id">
<span>{ { tarefa.titulo } }</span>
<button @click="removerTarefa(tarefa.id)" class="btn-remover">X</button>
</li>
</ul>
</main>
</template>
<style scoped>
/* Estilos para melhor visualização */
.erro {
color: red;
}
ul {
list-style: none;
padding: 0;
}
li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #ccc;
}
.btn-remover {
background-color: #ff4d4d;
color: white;
border: none;
cursor: pointer;
}
</style>
Nesta abordagem inicial, todas as classes (Entidade, Repositório e Controlador) estão no mesmo pacote raiz (br.com.tarefas.api). Embora funcional para projetos muito pequenos, essa estrutura não escala bem e viola o princípio de separação de conceitos.
src
└── main
└── java
└── br
└── com
└── tarefas
└── api
├── Tarefa.java // (Model)
├── TarefaRepository.java // (Repository - Data)
├── TarefaController.java // (Controller com lógica de negócio)
└── ApiApplication.java // (Classe principal)
Características:
api contém classes com responsabilidades distintas (acesso a dados, regras de negócio e apresentação).Controller está diretamente acoplado ao Repository, tornando difícil a reutilização da lógica de negócio e a implementação de testes unitários.A estrutura é reorganizada em pacotes que representam as camadas da arquitetura MVC. Cada pacote tem uma responsabilidade clara e bem definida.
src
└── main
└── java
└── br
└── com
└── tarefas
└── api
├── model // Pacote para entidades (Model)
| └── Tarefa.java
├── repository // Pacote para acesso a dados (Data)
| └── TarefaRepository.java
├── service // Pacote para regras de negócio (Service/Business)
| └── TarefaService.java
├── controller // Pacote para endpoints HTTP (Controller/Presentation)
| └── TarefaController.java
├── config // Pacote para classes de configuração (ex: CORS)
| └── WebConfig.java
└── ApiApplication.java // (Classe principal)
Características:
controller, service).Controller depende do Service, e o Service depende do Repository. As camadas comunicam-se através de interfaces bem definidas, facilitando a substituição de implementações e os testes.service), os endpoints (controller) ou o acesso a dados (repository) estão implementados.Usuario.java em model, UsuarioRepository em repository, e assim por diante.application.propertiesO arquivo application.properties é o principal local para configurar o comportamento da sua aplicação Spring Boot, como a conexão com o banco de dados, a porta do servidor e outras propriedades essenciais.
Localização: src/main/resources/application.properties
application.properties# ===================================================================
# CONFIGURACAO DO SERVIDOR WEB EMBUTIDO (TOMCAT)
# ===================================================================
# Define a porta em que a aplicacao backend sera executada.
# O padrao e 8080. Usar 8088 evita conflitos com outras aplicacoes.
server.port=8088
# Define um prefixo para todos os endpoints da aplicacao.
# Ajuda a organizar as rotas e a evitar colisoes, especialmente em ambientes com proxy.
# Ex: http://localhost:8088/api/tarefas em vez de http://localhost:8088/tarefas
server.servlet.context-path=/api
# ===================================================================
# CONFIGURACAO DO BANCO DE DADOS (DATASOURCE)
# ===================================================================
# URL de conexao para o banco de dados em memoria H2.
# 'mem' indica que e em memoria (dados sao perdidos ao reiniciar).
# 'tarefasdb' e o nome do banco de dados.
spring.datasource.url=jdbc:h2:mem:tarefasdb
# Driver JDBC para o H2. O Spring Boot geralmente o detecta automaticamente.
spring.datasource.driverClassName=org.h2.Driver
# Nome de usuario para acessar o banco de dados H2.
# 'sa' e o padrao (System Administrator).
spring.datasource.username=sa
# Senha para o banco de dados H2.
# Para desenvolvimento, uma senha em branco e comum.
spring.datasource.password=
# ===================================================================
# CONFIGURACAO DO H2 CONSOLE (INTERFACE WEB PARA O BANCO DE DADOS)
# ===================================================================
# Habilita o console web do H2, uma ferramenta util para visualizar
# e manipular os dados do banco diretamente no navegador.
spring.h2.console.enabled=true
# Define o caminho para acessar o console H2.
# Voce podera acessa-lo em: http://localhost:8088/api/h2-console
spring.h2.console.path=/h2-console
# ===================================================================
# CONFIGURACAO DO JPA (JAVA PERSISTENCE API) E HIBERNATE
# ===================================================================
# Dialeto do banco de dados que o Hibernate (implementacao JPA do Spring) deve usar.
# Isso permite que o Hibernate gere o SQL correto para o H2.
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# Acao a ser tomada no schema do banco de dados na inicializacao.
# 'create-drop': Cria o schema (tabelas) quando a aplicacao inicia e o destroi quando a aplicacao para.
# Perfeito para desenvolvimento, pois garante um ambiente limpo a cada reinicio.
# Outras opcoes: 'update', 'validate', 'none'.
spring.jpa.hibernate.ddl-auto=create-drop
# Mostra no console o SQL gerado pelo Hibernate.
# Extremamente util para depuracao e para entender o que esta acontecendo no banco de dados.
spring.jpa.show-sql=true
# Formata o SQL exibido no console para ser mais legivel.
spring.jpa.properties.hibernate.format_sql=true
application.properties pelo código acima.http://localhost:8088.http://localhost:8088/api/tarefas (note o /api adicionado).http://localhost:8088/h2-console.JDBC URL seja exatamente o mesmo definido em spring.datasource.url: jdbc:h2:mem:tarefasdb.sa e deixe a senha em branco.TAREFA, ver os dados que foram inseridos pela classe DataInitializer e até mesmo executar queries SQL diretamente.create table...).GET /api/tarefas, o SQL formatado (select ... from tarefa) aparecerá no console, mostrando exatamente a consulta que o show-sql=true revelou.Para popular o banco de dados com dados iniciais toda vez que a aplicação é iniciada, a abordagem mais comum e didática no Spring Boot é utilizar a interface CommandLineRunner. Ela garante que um código seja executado após a inicialização da aplicação e de todos os seus componentes.
Crie a classe dentro do pacote config, pois sua função é configurar um estado inicial para a aplicação.
Caminho: src/main/java/br/com/tarefas/api/config/DataInitializer.java
DataInitializerpackage br.com.tarefas.api.config;
import br.com.tarefas.api.model.Tarefa;
import br.com.tarefas.api.repository.TarefaRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* Componente que executa um código na inicialização da aplicação Spring Boot.
* Ideal para popular o banco de dados com dados de teste para desenvolvimento.
*/
@Component // Anotação que transforma a classe em um Bean gerenciado pelo Spring.
public class DataInitializer implements CommandLineRunner {
// Injeção de dependência do repositório de tarefas.
// O Spring fornecerá uma instância de TarefaRepository para esta classe.
private final TarefaRepository tarefaRepository;
@Autowired // Boas práticas recomendam a injeção via construtor.
public DataInitializer(TarefaRepository tarefaRepository) {
this.tarefaRepository = tarefaRepository;
}
/**
* Este método é executado automaticamente pelo Spring Boot na inicialização.
* @param args Argumentos de linha de comando (não utilizados aqui).
* @throws Exception
*/
@Override
public void run(String... args) throws Exception {
// Limpa o repositório para evitar duplicação a cada reinicialização
tarefaRepository.deleteAll();
// Criação de uma lista de tarefas iniciais com temas relevantes ao projeto
Tarefa t1 = new Tarefa();
t1.setTitulo("Configurar o backend Spring Boot com MVC");
t1.setConcluida(true);
Tarefa t2 = new Tarefa();
t2.setTitulo("Criar a entidade Tarefa e o Repository");
t2.setConcluida(true);
Tarefa t3 = new Tarefa();
t3.setTitulo("Desenvolver a camada de Serviço e o Controller");
t3.setConcluida(false);
Tarefa t4 = new Tarefa();
t4.setTitulo("Estudar e configurar o CORS para o frontend");
t4.setConcluida(false);
Tarefa t5 = new Tarefa();
t5.setTitulo("Inicializar o projeto Vue.js com a CLI");
t5.setConcluida(false);
Tarefa t6 = new Tarefa();
t6.setTitulo("Criar um serviço no frontend para consumir a API");
t6.setConcluida(false);
Tarefa t7 = new Tarefa();
t7.setTitulo("Desenvolver o componente principal para listar e adicionar tarefas");
t7.setConcluida(false);
// Salva todas as tarefas no banco de dados de uma só vez
// O método saveAll é mais performático do que salvar uma por uma em um loop.
tarefaRepository.saveAll(Arrays.asList(t1, t2, t3, t4, t5, t6, t7));
// Feedback no console para o desenvolvedor
System.out.println("✅ Base de dados inicializada com 7 tarefas.");
}
}
DataInitializerAnotação @Component: Esta anotação informa ao Spring que ele deve criar e gerenciar um objeto (um Bean) desta classe. Sem isso, a classe seria ignorada pela aplicação.
Interface CommandLineRunner: Ao implementar esta interface, obrigamos a classe a ter um método run(String... args). O Spring Boot identifica todos os Beans que implementam CommandLineRunner e executa seus métodos run logo após a aplicação iniciar completamente.
Injeção de Dependência (@Autowired): A classe precisa do TarefaRepository para salvar dados no banco. Em vez de criá-lo manualmente (new TarefaRepository()), declaramos que precisamos dele no construtor. O Spring, que já gerencia o TarefaRepository, automaticamente “injeta” ou fornece a instância pronta para uso. Esta é uma prática fundamental chamada Inversão de Controle (IoC).
Método run:
Tarefa normais, como faríamos em qualquer código Java.set para popular os objetos com títulos e status relevantes para o projeto do aluno.tarefaRepository.saveAll() recebe uma lista de objetos e os salva no banco de dados. É mais eficiente para múltiplas inserções.System.out.println é um feedback simples, mas crucial para o desenvolvedor saber que o processo de seed foi executado com sucesso ao olhar o console da aplicação.Não é necessário chamar esta classe em nenhum lugar. Apenas reinicie sua aplicação Spring Boot. As anotações e a implementação da interface farão com que o Spring a execute automaticamente. Você verá a mensagem “✅ Base de dados inicializada com 7 tarefas.” no log de inicialização e, ao acessar o endpoint GET /api/tarefas, os dados já estarão lá.
O erro “Falha ao carregar tarefas” é causado por uma incompatibilidade de porta entre a configuração do frontend (Vue) e a do backend (Spring Boot).
Analisando os arquivos, a causa do problema é a seguinte:
application.properties foi configurado para executar o servidor na porta 8088.tarefaService.js está tentando fazer requisições para a porta 8080.O navegador tenta contatar localhost:8080, não encontra nenhum serviço respondendo e a requisição falha, acionando a mensagem de erro no bloco catch do seu componente Vue.
Para corrigir, você deve alinhar a baseURL no arquivo tarefaService.js com a porta definida no seu backend.
Arquivo a ser modificado: frontend/app-tarefas/src/services/tarefaService.js
Código Antigo (Incorreto):
//...
const apiClient = axios.create({
baseURL: 'http://localhost:8080/api', // URL base da API
//...
Código Novo (Correto):
import axios from "axios";
// Instância do Axios com baseURL centralizada
const apiClient = axios.create({
baseURL: "http://localhost:8088/api", // URL base da API CORRIGIDA
headers: {
"Content-Type": "application/json",
},
});
// Exporta funções específicas para cada endpoint
export default {
getTarefas() {
return apiClient.get("/tarefas");
},
addTarefa(tarefa) {
return apiClient.post("/tarefas", tarefa);
},
deleteTarefa(id) {
return apiClient.delete(`/tarefas/${id}`);
},
};
Próximos Passos:
baseURL no arquivo tarefaService.js de 8080 para 8088.npm run dev) irá recarregar automaticamente.http://localhost:5173). O erro deve desaparecer e a lista de tarefas, populada pela classe DataInitializer, será exibida.Para ter 100% de certeza, siga estes passos no seu navegador (onde a aplicação Vue está aberta):
Abra as Ferramentas de Desenvolvedor: Pressione a tecla F12 (ou Ctrl+Shift+I / Cmd+Opt+I).
Vá para a Aba “Console”: Atualize a página (F5). Você verá uma mensagem de erro detalhada em vermelho, parecida com esta:
Access to XMLHttpRequest at ‘http://localhost:8088/api/tarefas’ from origin ‘http://localhost:5173’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
Vá para a Aba “Network” (Rede):
tarefas aparecer na lista, provavelmente em vermelho.200 OK, mas sim algo como (failed) e o tipo será cors.Essa mensagem confirma que o servidor backend (na porta 8088) não está dando permissão explícita para que o frontend (na porta 5173) acesse seus recursos.
Vamos garantir que a configuração de CORS no seu backend Spring Boot está correta e sendo carregada. A melhor abordagem é a configuração global, pois ela centraliza a regra em um único lugar.
Verifique a Porta no Frontend (Mais uma vez):
O arquivo tarefaService.js que você enviou ainda mostra a porta 8080. Este é o primeiro ponto a ser corrigido. Garanta que a baseURL esteja correta e que o arquivo foi salvo.
Arquivo: frontend/app-tarefas/src/services/tarefaService.js
// ...
const apiClient = axios.create({
baseURL: "http://localhost:8088/api", // GARANTA QUE ESTÁ 8088
// ...
});
// ...
Crie ou Verifique a Classe de Configuração de CORS no Backend: Esta é a solução recomendada e mais robusta. Se você ainda não criou este arquivo, crie-o exatamente como abaixo.
Localização: backend/src/main/java/br/com/tarefas/api/config/WebConfig.java
package br.com.tarefas.api.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // Aplica a política para TODAS as rotas
.allowedOrigins("http://localhost:5173") // Origem do frontend Vue
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // Métodos HTTP permitidos
.allowedHeaders("*") // Permite todos os cabeçalhos
.allowCredentials(true);
}
}
addMapping("/**") garante que a regra de CORS se aplique a todos os controllers da sua API, não apenas ao /api/tarefas.Reinicie o Servidor Backend:
Este passo é crucial. Pare completamente sua aplicação Spring Boot e inicie-a novamente. As classes de @Configuration só são lidas durante a inicialização.
Limpe o Cache e Teste Novamente:
Ctrl+F5 ou Cmd+Shift+R).Na aba Network das ferramentas de desenvolvedor do navegador, observe a linha Request URL:
Request URL: http://localhost:8080/api/tarefasIsso mostra inequivocamente que a requisição está sendo enviada para a porta 8080.
No entanto, o seu backend Spring Boot está configurado para rodar na porta 8088. A requisição falha porque não há nenhum servidor escutando na porta 8080, e a chamada nem chega a ser recebida pelo backend para que a política de CORS seja avaliada.
A correção continua a mesma: você precisa alterar o arquivo de serviço no frontend.
frontend/services/tarefaService.js.baseURL.8080 para 8088.Código a ser corrigido em tarefaService.js:
import axios from "axios";
const apiClient = axios.create({
// 👇 AQUI ESTÁ O ERRO
baseURL: "http://localhost:8080/api",
// ...
});
Código Corrigido:
import axios from "axios";
const apiClient = axios.create({
// 👇 CORREÇÃO APLICADA
baseURL: "http://localhost:8088/api",
headers: {
"Content-Type": "application/json",
},
});
// O resto do arquivo permanece igual...
export default {
getTarefas() {
return apiClient.get("/tarefas");
},
addTarefa(tarefa) {
return apiClient.post("/tarefas", tarefa);
},
deleteTarefa(id) {
return apiClient.delete(`/tarefas/${id}`);
},
};
Passos Finais:
tarefaService.js, observe o terminal onde npm run dev está rodando para garantir que ele recompilou o projeto sem erros.F5). A requisição será feita para a porta correta (8088) e a aplicação deverá funcionar.Com base nos logs do console e na aba Network, o problema é um erro 404 Not Found causado por uma duplicação no caminho (path) da URL da sua API.
O seu backend está esperando uma URL e o seu frontend está chamando outra.
Configuração do Backend: No seu application.properties, você configurou server.servlet.context-path=/api. Isso significa que o Spring Boot automaticamente adiciona o prefixo /api a todos os endpoints da sua aplicação.
Mapeamento do Controller: No seu TarefaController.java, o endpoint está mapeado como @RequestMapping("/api/tarefas").
O Problema (A Duplicação): O Spring Boot combina os dois caminhos. O context-path e o @RequestMapping.
/api/api/tarefas/api/api/tarefasRequisição do Frontend: Seu frontend está configurado para chamar http://localhost:8080/api/tarefas, como mostra a imagem.
A requisição do frontend em /api/tarefas não encontra o endpoint, pois o servidor o registrou em /api/api/tarefas. Por isso, o servidor responde corretamente com 404 Not Found.
Você tem duas maneiras de corrigir isso. A primeira é a mais recomendada por ser mais limpa e evitar redundância.
Altere o mapeamento do seu controller para não repetir o /api, já que ele é gerenciado globalmente pelo context-path.
Abra o arquivo: TarefaController.java
Altere a anotação @RequestMapping:
Código Antigo:
// ...
@RestController
@RequestMapping("/api/tarefas") // <-- Caminho redundante
public class TarefaController {
// ...
}
Código Novo (Correto):
// ...
@RestController
@RequestMapping("/tarefas") // <-- Correção: Removido o "/api"
public class TarefaController {
// ...
}
Reinicie sua aplicação Spring Boot. As alterações no backend só têm efeito após a reinicialização.
context-path do BackendSe você preferir que cada controller defina seu caminho completo, você pode remover a configuração global.
Abra o arquivo: application.properties
Comente ou remova a linha:
Código Antigo:
# ...
server.servlet.context-path=/api
# ...
Código Novo (Correto):
# ...
# server.servlet.context-path=/api
# ...
Reinicie sua aplicação Spring Boot.
mvn spring-boot:run
Agora que a comunicação está estabelecida, vamos implementar o CRUD completo e modernizar o visual.`
Vamos adicionar as funcionalidades de Update (Atualizar). Teremos duas formas de atualização:
PUT: Para marcar uma tarefa como concluída/não concluída.PATCH: Para editar o título de uma tarefa.TarefaService.java)Adicione os métodos para atualizar a tarefa.
Arquivo: src/main/java/br/com/tarefas/api/service/TarefaService.java
package br.com.tarefas.api.service;
import br.com.tarefas.api.model.Tarefa;
import br.com.tarefas.api.repository.TarefaRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional; // Importar Optional
@Service
public class TarefaService {
@Autowired
private TarefaRepository tarefaRepository;
public List<Tarefa> listarTodas() {
return tarefaRepository.findAll();
}
public Tarefa criarTarefa(Tarefa tarefa) {
if (tarefa.getTitulo() == null || tarefa.getTitulo().isBlank()) {
throw new IllegalArgumentException("O título da tarefa é obrigatório.");
}
return tarefaRepository.save(tarefa);
}
// --- NOVOS MÉTODOS ADICIONADOS ---
/**
* Atualiza o status de conclusão de uma tarefa.
* @param id O ID da tarefa a ser atualizada.
* @param concluida O novo status de conclusão.
* @return A tarefa atualizada.
*/
public Tarefa atualizarStatus(Long id, boolean concluida) {
// orElseThrow lança uma exceção se a tarefa não for encontrada.
Tarefa tarefaExistente = tarefaRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Tarefa não encontrada com o id: " + id));
tarefaExistente.setConcluida(concluida);
return tarefaRepository.save(tarefaExistente);
}
/**
* Atualiza o título de uma tarefa.
* @param id O ID da tarefa a ser atualizada.
* @param titulo O novo título.
* @return A tarefa atualizada.
*/
public Tarefa atualizarTitulo(Long id, String titulo) {
if (titulo == null || titulo.isBlank()) {
throw new IllegalArgumentException("O novo título não pode ser vazio.");
}
Tarefa tarefaExistente = tarefaRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Tarefa não encontrada com o id: " + id));
tarefaExistente.setTitulo(titulo);
return tarefaRepository.save(tarefaExistente);
}
public void deletarTarefa(Long id) {
if (!tarefaRepository.existsById(id)) {
throw new RuntimeException("Tarefa não encontrada com o id: " + id);
}
tarefaRepository.deleteById(id);
}
}
TarefaController.java)Adicione os novos endpoints (PUT e PATCH).
Arquivo: src/main/java/br/com/tarefas/api/controller/TarefaController.java
package br.com.tarefas.api.controller;
import br.com.tarefas.api.model.Tarefa;
import br.com.tarefas.api.service.TarefaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map; // Importar Map
@RestController
@RequestMapping("/tarefas") // Caminho corrigido para não duplicar /api
public class TarefaController {
@Autowired
private TarefaService tarefaService;
@GetMapping
public ResponseEntity<List<Tarefa>> listar() {
List<Tarefa> tarefas = tarefaService.listarTodas();
return ResponseEntity.ok(tarefas);
}
@PostMapping
public ResponseEntity<Tarefa> criar(@RequestBody Tarefa tarefa) {
Tarefa novaTarefa = tarefaService.criarTarefa(tarefa);
return new ResponseEntity<>(novaTarefa, HttpStatus.CREATED);
}
// --- NOVOS ENDPOINTS ADICIONADOS ---
@PutMapping("/{id}/status")
public ResponseEntity<Tarefa> atualizarStatus(@PathVariable Long id, @RequestBody Map<String, Boolean> status) {
// @RequestBody Map... permite receber um JSON simples como {"concluida": true}
Tarefa tarefaAtualizada = tarefaService.atualizarStatus(id, status.get("concluida"));
return ResponseEntity.ok(tarefaAtualizada);
}
@PatchMapping("/{id}/titulo")
public ResponseEntity<Tarefa> atualizarTitulo(@PathVariable Long id, @RequestBody Map<String, String> payload) {
Tarefa tarefaAtualizada = tarefaService.atualizarTitulo(id, payload.get("titulo"));
return ResponseEntity.ok(tarefaAtualizada);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletar(@PathVariable Long id) {
tarefaService.deletarTarefa(id);
return ResponseEntity.noContent().build();
}
}
Importante: Reinicie seu servidor backend para aplicar as mudanças.
tarefaService.js)Adicione as funções para se comunicar com os novos endpoints.
Arquivo: frontend/app-tarefas/src/services/tarefaService.js
import axios from "axios";
const apiClient = axios.create({
baseURL: "http://localhost:8088/api", // Verifique a porta e o /api
headers: {
"Content-Type": "application/json",
},
});
export default {
getTarefas() {
return apiClient.get("/tarefas");
},
addTarefa(tarefa) {
return apiClient.post("/tarefas", tarefa);
},
deleteTarefa(id) {
return apiClient.delete(`/tarefas/${id}`); }, // --- NOVAS FUNÇÕES ADICIONADAS --- updateStatusTarefa(id, concluida) { return apiClient.put(`/tarefas/${id}/status`, { concluida });
},
updateTituloTarefa(id, titulo) {
return apiClient.patch(`/tarefas/${id}/titulo`, { titulo });
},
};
App.vue)Esta é a maior mudança. Adicionaremos reatividade, métodos para edição e um <style> completamente novo.
Arquivo: frontend/src/App.vue
<script setup>
import { ref, onMounted, nextTick } from "vue";
import tarefaService from "./services/tarefaService";
const tarefas = ref([]);
const novaTarefaTitulo = ref("");
const erro = ref(null);
const tarefaEditandoId = ref(null); // ID da tarefa sendo editada
const tituloEditando = ref(""); // Título temporário durante a edição
// Funções de API
async function carregarTarefas() {
try {
erro.value = null;
const response = await tarefaService.getTarefas();
// Ordena as tarefas: pendentes primeiro, depois por ID
tarefas.value = response.data.sort(
(a, b) => a.concluida - b.concluida || a.id - b.id
);
} catch (e) {
erro.value = "Falha ao carregar tarefas.";
console.error(e);
}
}
async function adicionarTarefa() {
if (!novaTarefaTitulo.value.trim()) return;
const novaTarefa = { titulo: novaTarefaTitulo.value, concluida: false };
try {
erro.value = null;
const response = await tarefaService.addTarefa(novaTarefa);
tarefas.value.push(response.data);
novaTarefaTitulo.value = "";
// Recarrega para manter a ordem
await carregarTarefas();
} catch (e) {
erro.value = "Falha ao adicionar tarefa.";
console.error(e);
}
}
async function removerTarefa(id) {
try {
erro.value = null;
await tarefaService.deleteTarefa(id);
tarefas.value = tarefas.value.filter((t) => t.id !== id);
} catch (e) {
erro.value = "Falha ao remover tarefa.";
console.error(e);
}
}
// --- NOVAS FUNÇÕES ---
async function alternarStatus(tarefa) {
try {
erro.value = null;
const novoStatus = !tarefa.concluida;
const response = await tarefaService.updateStatusTarefa(
tarefa.id,
novoStatus
);
// Atualiza o objeto local para reatividade instantânea
tarefa.concluida = response.data.concluida;
// Recarrega para reordenar a lista
await carregarTarefas();
} catch (e) {
erro.value = "Falha ao atualizar status.";
console.error(e);
}
}
function ativarEdicao(tarefa) {
tarefaEditandoId.value = tarefa.id;
tituloEditando.value = tarefa.titulo;
// nextTick garante que o DOM foi atualizado antes de focar no input
nextTick(() => {
document.querySelector(`#input-edit-${tarefa.id}`).focus();
});
}
async function salvarEdicao(tarefa) {
if (
!tituloEditando.value.trim() ||
tituloEditando.value.trim() === tarefa.titulo
) {
cancelarEdicao();
return;
}
try {
erro.value = null;
const response = await tarefaService.updateTituloTarefa(
tarefa.id,
tituloEditando.value
);
tarefa.titulo = response.data.titulo;
cancelarEdicao();
} catch (e) {
erro.value = "Falha ao salvar edição.";
console.error(e);
}
}
function cancelarEdicao() {
tarefaEditandoId.value = null;
tituloEditando.value = "";
}
// Carrega as tarefas quando o componente é montado
onMounted(carregarTarefas);
</script>
<template>
<div class="container">
<header>
<h1>Lista de Tarefas</h1>
<p>Aplicação Full-Stack com Vue e Spring Boot</p>
</header>
<main>
<div class="form-container">
<input
v-model="novaTarefaTitulo"
@keyup.enter="adicionarTarefa"
placeholder="O que precisa ser feito?"
/>
<button @click="adicionarTarefa">+</button>
</div>
<p v-if="erro" class="erro">{ { erro } }</p>
<ul class="tarefa-lista">
<li
v-for="tarefa in tarefas"
:key="tarefa.id"
:class="{ concluida: tarefa.concluida }"
>
<div class="info-tarefa">
<input
type="checkbox"
:checked="tarefa.concluida"
@change="alternarStatus(tarefa)"
/>
<span
v-if="tarefaEditandoId !== tarefa.id"
@dblclick="ativarEdicao(tarefa)"
>
{ { tarefa.titulo } }
</span>
<input
v-else
:id="`input-edit-${tarefa.id}`"
type="text"
v-model="tituloEditando"
@blur="salvarEdicao(tarefa)"
@keyup.enter="salvarEdicao(tarefa)"
@keyup.esc="cancelarEdicao"
class="input-edicao"
/>
</div>
<div class="acoes">
<button @click="removerTarefa(tarefa.id)" class="btn-remover">
×
</button>
</div>
</li>
</ul>
</main>
</div>
</template>
<style>
/* Reset Básico e Estilo Global */
:root {
--cor-fundo: #1a1a1a;
--cor-surface: #242424;
--cor-primaria: #646cff;
--cor-texto: rgba(255, 255, 255, 0.87);
--cor-borda: #424242;
--cor-perigo: #ff6b6b;
}
body {
font-family: "Inter", system-ui, Avenir, Helvetica, Arial, sans-serif;
background-color: var(--cor-fundo);
color: var(--cor-texto);
margin: 0;
display: flex;
justify-content: center;
padding-top: 40px;
}
/* Container Principal */
.container {
width: 100%;
max-width: 680px;
margin: 0 auto;
padding: 0 20px;
}
header {
text-align: center;
margin-bottom: 40px;
}
header h1 {
font-size: 3.2em;
margin: 0;
color: var(--cor-primaria);
}
header p {
color: #a0a0a0;
}
/* Formulário de Nova Tarefa */
.form-container {
display: flex;
margin-bottom: 20px;
gap: 10px;
}
.form-container input {
flex-grow: 1;
padding: 12px 15px;
font-size: 1em;
background-color: var(--cor-surface);
border: 1px solid var(--cor-borda);
border-radius: 8px;
color: var(--cor-texto);
outline: none;
transition: border-color 0.2s;
}
.form-container input:focus {
border-color: var(--cor-primaria);
}
.form-container button {
padding: 0;
width: 48px;
height: 48px;
font-size: 2em;
border-radius: 8px;
border: 1px solid transparent;
background-color: var(--cor-primaria);
color: white;
cursor: pointer;
transition: background-color 0.2s;
line-height: 48px;
}
.form-container button:hover {
background-color: #535bf2;
}
/* Mensagem de Erro */
.erro {
color: var(--cor-perigo);
text-align: center;
margin-bottom: 20px;
}
/* Lista de Tarefas */
.tarefa-lista {
list-style: none;
padding: 0;
margin: 0;
}
.tarefa-lista li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background-color: var(--cor-surface);
border-bottom: 1px solid var(--cor-borda);
transition: background-color 0.2s;
}
.tarefa-lista li:first-child {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.tarefa-lista li:last-child {
border-bottom: none;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
.info-tarefa {
display: flex;
align-items: center;
gap: 15px;
flex-grow: 1;
}
/* Checkbox customizado */
.info-tarefa input[type="checkbox"] {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid var(--cor-borda);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.info-tarefa input[type="checkbox"]:hover {
border-color: var(--cor-primaria);
}
.info-tarefa input[type="checkbox"]:checked {
background-color: var(--cor-primaria);
border-color: var(--cor-primaria);
}
.info-tarefa input[type="checkbox"]:checked::after {
content: "✔";
color: white;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;
}
.info-tarefa span {
cursor: pointer;
}
/* Estilo da tarefa concluída */
li.concluida .info-tarefa span {
text-decoration: line-through;
color: #a0a0a0;
}
/* Input de Edição */
.input-edicao {
font-size: 1em;
padding: 5px;
background-color: var(--cor-fundo);
border: 1px solid var(--cor-primaria);
border-radius: 4px;
color: var(--cor-texto);
flex-grow: 1;
}
/* Botão de Remover */
.btn-remover {
background: none;
border: none;
color: #a0a0a0;
font-size: 1.5em;
cursor: pointer;
transition: color 0.2s;
visibility: hidden;
opacity: 0;
}
li:hover .btn-remover {
visibility: visible;
opacity: 1;
}
.btn-remover:hover {
color: var(--cor-perigo);
}
</style>
—`
No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd fullstack_tarefas_06_spring_vue
./mvnw spring-boot:run
# No frontend: npm install && npm run dev
[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta navegar até a pasta antes de executar: cd proj_aplicacoes_full_stack/projetos/fullstack_tarefas_06_spring_vue