Criando uma API de lista de tarefas usando Java Spring Boot
💡 TLDR
Neste post, vamos construir um simples aplicativo de lista de tarefas TODO usando Java e o framework Spring Boot. O Spring Boot torna fácil criar aplicativos independentes com qualidade de produção com configuração mínima. Vamos usá-lo para construir uma API RESTful e persistir nossos dados em um banco de dados H2 em memória.
Pré-requisitos Antes de começar, certifique-se de ter instalado:
- Java 11+
- Maven 3+
- Uma IDE como VSCode, IntelliJ IDEA ou Eclipse Você pode usar este guia para configurar os pré-requisitos
Também vamos usar as seguintes bibliotecas e ferramentas:
- Spring Boot - para construir o aplicativo web
- Spring Data JPA - para interagir com o banco de dados
- Banco de dados H2 - um banco de dados SQL em memória
- Maven - para gerenciar dependências
Configuração do Projeto Vamos começar criando nosso projeto no Spring Initializr. Isso nos dá um projeto Maven básico com o Spring Boot já configurado.
- Crie um projeto Maven com Java 17
- Adicione as dependências Spring Web e Spring Data JPA
- Clique em Gerar para baixar o projeto
Extraia o projeto baixado e abra-o em sua IDE. A classe principal do aplicativo é TodoApplication.java em src/main/java.
E seus pacotes vão ficar dentro da estrutura de pastas que você colocar em Group, no meu caso coloquei br.com.abilioazevedo. Então vou ter as pastas: src/main/java/br/com/abilioazevedo/todolist.
Criando Módulo User
Vamos criar o módulo para gerenciar nossos usuários. Criamos uma pasta: src/main/java/br/com/abilioazevedo/todolist/user
Primeiro o Repositório (IUserRepository.java) para comunicação com o banco.
package br.com.abilioazevedo.todolist.user;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
public interface IUserRepository extends JpaRepository<UserModel, UUID>{
UserModel findByUsername(String username);
}
Depois o Model (UserModel.java)
package br.com.abilioazevedo.todolist.user;
import java.time.LocalDateTime;
import java.util.UUID;
import org.hibernate.annotations.CreationTimestamp;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Data;
@Data
@Entity(name = "tb_users")
public class UserModel {
@Id
@GeneratedValue(generator = "UUID")
private UUID id;
@Column(unique = true)
private String username;
private String name;
private String password;
@CreationTimestamp
private LocalDateTime createAt;
}
E por fim o Controller (UserController.java) para criar o usuário:
package br.com.abilioazevedo.todolist.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import at.favre.lib.crypto.bcrypt.BCrypt;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private IUserRepository userRepository;
@PostMapping()
public ResponseEntity create(@RequestBody UserModel userModel) {
var user = this.userRepository.findByUsername(userModel.getUsername());
if (user != null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Usuário já cadastrado");
}
var passwordHashed = BCrypt.withDefaults().hashToString(12, userModel.getPassword().toCharArray());
userModel.setPassword(passwordHashed);
var userCreated = this.userRepository.save(userModel);
return ResponseEntity.status(HttpStatus.CREATED).body(userCreated);
}
}
Criando o middleware de autenticação
Agora que podemos criar nosso usuário, precisamos autenticá-lo e acessar o id nas rotas de tarefas. Para isso, vamos criar um filtro que irá identificar os parâmetros de basic auth do headers e buscar o usuário equivalente.
Vamos criar o arquivo filter/FilterTaskAuth.java:
package br.com.abilioazevedo.todolist.filter;
import java.io.IOException;
import java.util.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import at.favre.lib.crypto.bcrypt.BCrypt;
import br.com.abilioazevedo.todolist.user.IUserRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class FilterTaskAuth extends OncePerRequestFilter {
@Autowired
private IUserRepository userRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
var servletPath = request.getServletPath();
if(!servletPath.startsWith("/task")){
filterChain.doFilter(request, response);
return;
}
var authorization = request.getHeader("Authorization");
var authEncoded = authorization.substring("Basic".length()).trim();
byte[] authDecoded = Base64.getDecoder().decode(authEncoded);
var authString = new String(authDecoded);
String[] credentials = authString.split(":");
String username = credentials[0];
String password = credentials[1];
var user = this.userRepository.findByUsername(username);
if (user == null) {
response.sendError(401, "Usuário não encontrado");
}else{
var passwordHashed = user.getPassword();
var passwordMatch = BCrypt.verifyer().verify(password.toCharArray(), passwordHashed).verified;
if (!passwordMatch) {
response.sendError(401, "Senha inválida");
}else{
request.setAttribute("userId", user.getId());
filterChain.doFilter(request, response);
}
}
}
}
Perceba que o filtro/middleware só é aplicado para rotas que começam com task.
Criando Módulo Task
Vamos criar o módulo para gerenciar nossos usuários. Criamos uma pasta: src/main/java/br/com/abilioazevedo/todolist/task Primeiro o Repositório (ITaskRepository.java) para comunicação com o banco.
package br.com.abilioazevedo.todolist.user;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
public interface IUserRepository extends JpaRepository<UserModel, UUID>{
UserModel findByUsername(String username);
}
Isso irá lidar com todas as operações CRUD para entidades Todo e também estamos adicionando uma nova função das padrão findByIdUser
Depois, podemos criar o Modelo de Tarefa (TaskModel.java):
package br.com.abilioazevedo.todolist.task;
import java.time.LocalDateTime;
import java.util.UUID;
import org.hibernate.annotations.CreationTimestamp;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Data;
@Data
@Entity(name = "tb_tasks")
public class TaskModel {
@Id
@GeneratedValue(generator = "UUID")
private UUID id;
@Column(length = 50)
private String title;
private String description;
private UUID idUser;
private LocalDateTime startAt;
private LocalDateTime endAt;
private String priority;
private Boolean done;
@CreationTimestamp
private LocalDateTime createAt;
public void setTitle(String title) throws Exception {
if (title.length() > 50) {
throw new Exception("O campo title deve conter no máximo 50 caracteres");
}
this.title = title;
}
}
Esta é uma entidade JPA que será persistida no banco de dados. Ela tem um id autogerado, um título, descrição, o id do usuário, quando a tarefa começa e quando termina, a prioridade e uma flag para indicar a conclusão.
Por fim, criamos o controller que irá lidar com todas as operações CRUD para entidades Task (TaskController.java):
package br.com.abilioazevedo.todolist.task;
import java.time.LocalDateTime;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import br.com.abilioazevedo.todolist.utils.Utils;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/task")
@SecurityRequirement(name = "basicAuth")
public class TaskController {
@Autowired
private ITaskRepository taskRepository;
@PostMapping()
public ResponseEntity create(@RequestBody TaskModel taskModel, HttpServletRequest request) {
var idUser = request.getAttribute("userId");
taskModel.setIdUser((UUID) idUser);
var currentDate = LocalDateTime.now();
if(currentDate.isAfter(taskModel.getStartAt())){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Data de início não pode ser menor que a data atual");
}
if(taskModel.getEndAt().isBefore(taskModel.getStartAt())){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Data de fim não pode ser menor que a data de início");
}
var taskCreated = this.taskRepository.save(taskModel);
return ResponseEntity.status(HttpStatus.CREATED).body(taskCreated);
}
@GetMapping()
public ResponseEntity findAll(HttpServletRequest request) {
var idUser = request.getAttribute("userId");
var tasks = this.taskRepository.findByIdUser((UUID) idUser);
return ResponseEntity.ok(tasks);
}
@PutMapping("/{id}")
public ResponseEntity update(@RequestBody TaskModel taskModel, @PathVariable UUID id, HttpServletRequest request) {
var task = this.taskRepository.findById(id).orElse(null);
if (task == null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Tarefa não encontrada");
}
var idUser = request.getAttribute("userId");
if (!task.getIdUser().equals(idUser)) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Usuário não tem permissão para alterar essa tarefa");
}
Utils.copyNonNullProperties(taskModel, task);
var taskUpdated = this.taskRepository.save(task);
return ResponseEntity.ok().body(taskUpdated);
}
}
Isso irá expor os endpoints para criar, ler, atualizar e excluir tarefas. OBS: Você pode ver que estamos usando a seguinte função Utils.copyNonNullProperties, ela serve para combinar as propriedades não nulas dos dois objetos, seu código está no repositório final aqui.
Configurando o Spring Data JPA
Em seguida, precisamos configurar o Spring Data JPA para conectar ao nosso banco de dados.
Abra src/main/resources/application.properties e adicione o seguinte:
spring.datasource.url=jdbc:h2:mem:todolist
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=admin
spring.datasource.password=admin
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
Isso configura um banco de dados H2 em memória que será automaticamente populado com nossas entidades.
CONSOLE DO BD
Você pode acessar o console H2: localhost:8080/h2-console
Testando a API
Nossa API básica agora está pronta! Vamos testá-la. Inicie o aplicativo executando:
mvn spring-boot:run
Em seguida, você pode enviar solicitações para: POST /api/v1/todos - Criar um nova tarefa GET /api/v1/todos - Obter todas as tarefas PUT /api/v1/todos/{id} - Atualizar uma tarefa DELETE /api/v1/todos/{id} - Excluir uma tarefa
Você pode usar Postman ou CURL para testar esses endpoints. Ou usando swagger acessando: http://localhost:8080/swagger-ui/index.html
OBS: Você pode ver a configuração do swagger no repositório final do projeto aqui.
Deploy
Você pode usar o https://render.com/ para implantar seu aplicativo, você só precisa conectar seu repositório e implantá-lo usando o Docker. Acesse o render para criar seu aplicativo web: https://dashboard.render.com/create?type=web
Você terá seu aplicativo em execução:
Você pode testar a aplicação aqui
Conclusão
Este é apenas um exemplo básico, mas você pode estendê-lo adicionando autenticação, mais modelos, lógica de negócios e outros recursos. O código completo está disponível aqui.