Abílio Azevedo.

Creating a To-Do List API using Java Spring Boot

Cover Image for Creating a To-Do List API using Java Spring Boot
Abílio Azevedo
Abílio Azevedo

In this post, we'll walk through building a simple TODO list web application using Java and the Spring Boot framework. Spring Boot makes it easy to create stand-alone, production-grade applications with minimal setup. We'll use it to build a RESTful API and persist our data to an in-memory H2 database.

Prerequisites

Before we begin, make sure you have the following installed:

  • Java 11+
  • Maven 3+
  • An IDE like VSCode or IntelliJ IDEA or Eclipse

You can use this guide

We'll also use the following key libraries and tools:

  • Spring Boot - for building the web application
  • Spring Data JPA - for interacting with the database
  • H2 Database - an in-memory SQL database
  • Maven - for managing dependencies

Project Setup

Let's start by generating our project on Spring Initializr. This gives us a basic Maven project with Spring Boot already configured.

Generate a Maven project with Java 17 Add Spring Web and Spring Data JPA dependencies Click Generate to download the project

start.spring.io

Extract the downloaded project and open it in your IDE. The main application class is TodoApplication.java under src/main/java.

And your packages will be inside the folder structure that you put in Group, in my case I put br.com.abilioazevedo. So I will have the folders: src/main/java/br/com.abilioazevedo/todolist.

Creating User Module

Let's create the module to manage our users. We create a folder: src/main/java/br.com.abilioazevedo/todolist/user

First the Repository (IUserRepository.java) for communication with the database.

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

After the 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;
}

And then the controller (UserController.java) to expose the endpoint to create the user:

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

Creating the authentication middleware

Now that we can create our user, we need to authenticate it and access the id in the task routes. For this, we will create a filter that will identify the basic auth parameters from the headers and search for the equivalent user.

Let's create the file 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);
        }
      }
  }
}

Notice that the filter/middleware is only applied to routes starting with task.

Creating Task Module

Let's create the module to manage our tasks. We create a folder: src/main/java/br.com.abilioazevedo/todolist/task

First the Repository (ITaskRepository.java) for communication with the database.

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

This will handle all the CRUD operations for Todo entities and we are also adding a new function from the defaults ones findByIdUser

After, we can create the Task Model (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;
  }
}

This is a JPA entity that will be persisted in the database. It has an auto-generated id, a title, description, the user id, when the task starts and ends, the priority and a flag to indicate completion.

Finally, we create the controller that will handle all the CRUD operations for Task entities (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);
  }
}

This will expose the endpoints to create, read, update and delete tasks.

NOTE: You can see we are using the following function Utils.copyNonNullProperties, it serves to combine the non-null properties of the two objects, its code is in the final repository here.

Setting up Spring Data JPA

Next, we need to configure Spring Data JPA to connect to our database.

Open src/main/resources/application.properties and add the following:

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

This configures an in-memory H2 database that will be automatically populated from our entities.

DB CONSOLE

You can access the H2 console: localhost:8080/h2-console H2 console credentials

H2 Console

Testing the API

Our basic API is now ready! Let's test it out.

Start the application by running this command:

mvn spring-boot:run

Then you can send requests to: POST /api/v1/todos - Create a new todo GET /api/v1/todos - Get all todos PUT /api/v1/todos/{id} - Update a todo DELETE /api/v1/todos/{id} - Delete a todo

You can use Postman or CURL to test these endpoints. Or using swagger by accessing: http://localhost:8080/swagger-ui/index.html Todo Swagger

NOTE: You can see the swagger configuration in the final project repository here.

Deploy it

You can use https://render.com/ to deploy your application, you just need to connect your repository and deploy it using Docker.

You can test it here

Summary

In this post, we built a simple TODO list app in Java using Spring Boot, Spring Data JPA, and H2. The key steps were:

  • Setting up the project with Spring Initializr
  • Creating a Todo JPA entity
  • Configuring Spring Data JPA
  • Building a REST controller
  • Testing the CRUD API endpoints The full code is available on GitHub.

This is just a basic example, but you can extend it by adding more models, business logic, and other features. Spring Boot and Spring Data make it easy to focus on the application logic rather than infrastructure concerns.

2023- Java Rocketseat


More posts

Cover Image for Tech Documents

Tech Documents

Learn the vital importance of comprehensive technical documentation for growing software projects. Discover best practices like Requests for Comments (RFCs) and Architectural Decision Records (ADRs) that promote transparency, collaboration, and architectural decision logging. Explore powerful tools like wiki.js and Backstage to create effective documentation hubs. Keep your project organized, comprehensible, and sustainable with this approach to technical documentation.

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

Superlógica - BFF para o Gruvi

Building a BFF (Backend for Frontend) for the Gruvi SuperApp that have more than 120k active users and millions of possible users to rollout in the Superlogica ecosystem.

Abílio Azevedo
Abílio Azevedo

NewsLetter

I will send the content posted here. No Spam =)