Abílio Azevedo.

Criando uma API de lista de tarefas usando Java Spring Boot

Cover Image for Criando uma API de lista de tarefas usando Java Spring Boot
Abílio Azevedo
Abílio Azevedo

💡 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

start.spring.io

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 H2 console credentials

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 Todo Swagger

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 render create app

Você terá seu aplicativo em execução: render

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.

2023- Java Rocketseat


Mais posts

Cover Image for Documentos Técnicos

Documentos Técnicos

Aprenda a importância vital da documentação técnica abrangente para projetos de software em crescimento. Descubra as melhores práticas, como Requests for Comments (RFCs) e Architectural Decision Records (ADRs), que promovem transparência, colaboração e registro de decisões arquiteturais. Explore ferramentas poderosas como wiki.js e Backstage para criar centros de documentação eficazes. Mantenha seu projeto organizado, compreensível e sustentável com essa abordagem à documentação técnica.

Abílio Azevedo
Abílio Azevedo
Cover Image for Superlógica - BFF para o Gruvi

Superlógica - BFF para o Gruvi

Construindo um BFF (Backend for Frontend) para o SuperApp Gruvi que tem mais de 120 mil usuários ativos e milhões de possíveis usuários para disponibilizar no ecossistema Superlogica.

Abílio Azevedo
Abílio Azevedo

NewsLetter

Eu enviarei o conteúdo postado aqui no blog. Sem Spam =)

Engenheiro de software experiente, formado em Engenharia Elétrica, com mais de 10 anos de experiência prática na construção de aplicativos móveis, web e back-end robustos e escaláveis em vários projetos, principalmente no setor de fintech. Mobile (React Native), Web (React e Next.JS) e Backend (Node.JS, PHP e DJANGO). Meu objetivo é criar produtos que agreguem valor às pessoas. - © 2024, Abílio Azevedo