Todo App using Spring Boot REST API

0. Overview

We will implement simple Todo List app, with client using jQuery and backend use Spring Boot REST APIs.

1. Installation

Spring Boot

If you have both Ruby on Rails and Java Spring boot on the mac,
when you type spring command, it will run spring from Ruby!

brew tap pivotal/tap
brew install springboot

To avoid that, let do symlink from Java spring to springboot

sudo ln -sf /usr/local/Cellar/springboot/*.RELEASE/bin/spring/usr/local/bin/springboot

Now for running spring command:

spring init --name=my-project --dependencies=web,data-jpa,mysql,devtools,thymeleaf --package-name=com.myproject TestApp

Visual Studio Code

Install Visual Studio Code with Extensions

PostgreSQL

brew install postgresql
brew services start postgresql

Create default database

createdb mydb
psql mydb
mydb=# create user demouser with password 'demouser_password'

That’s all for Postgre SQL, in case you use MySQL (more difficult to handle java UUID type).

CREATE DATABASE demodb;
USE demodb;
CREATE USER 'demouser'@'localhost' IDENTIFIED BY 'demouser_password';
GRANT ALL PRIVILEGES ON demodb.* TO 'demouser'@'localhost';

2. Create Project

Start Visual Studio Code, Create Spring Boot project by
CMD + SHIFT + P > Spring Initialze Generate Maven Project > .. follow the guide.

Adding below dependencies, SHIFF + CMD + P > Maven: Add dependency
Type below and add:

web: Starter for building web application, including RESTful, Spring MVC. And Tomcat as the default embedded container.
postgresql: JDBC Type driver for PostgreSQL
jpa: Starter for using Spring Data JPA with Hibernate
security: Starter for using Spring Security
actuator: Starter for using Spring Boot’s Actuator which provides production ready features to help you monitor and manage your application
devtool: additional set of tools that can make the application development experience a little more pleasant
lombok: Lombok is used to reduce boilerplate code for model/data objects, e.g., it can generate getters and setters for those object automatically by using Lombok annotations. The easiest way is to use the @Data annotation.

Changing application properties file:

spring.datasource.url = jdbc:postgresql://localhost:5432/nbt?useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username= demouser
spring.datasource.password= demouser_password

spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQL94Dialect
spring.jpa.hibernate.ddl-auto = update

logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type=TRACE

server.port = 4000

# FOR THYMELEAF
spring.thymeleaf.cache=false
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
 
# App name
spring.application.name=Todo List

Important note: allowPublicKeyRetrieval=true must be added to avoid any errors.

Create models, services, controllers folder inside our project.

3. Create Date Model

Using Lombok.Data

@Data, is an annotation class for Lombok project. It bundles the features of @ToString, @EqualsAndHashCode, @Getter, @Setterand@RequiredArgsConstructor together.
It generates all the boilerplate that is normally associated with simple POJOs (Plain Old Java Objects) and beans.

We can create TodoItem.java as below:

package com.example.demo.models;

import java.util.Date;
import java.util.UUID;
import javax.persistence.*;
import javax.validation.constraints.NotEmpty;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Entity
@AllArgsConstructor 
@NoArgsConstructor
@Table(name= "todoitems")
public class TodoItem {
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    @Column(name="itemId")
    private Long itemId;

    @Column(name="listId")
    @org.hibernate.annotations.Type(type="pg-uuid")
    private UUID listId;

    @Column(name="taskName")
    @NotEmpty(message="* Enter Task Name")
    private String taskName;

    @Column(name="isDone")
    private Boolean isDone = false; // Default value

    @Column(name="createdAt")
    private Date createdAt = new Date();
}

Note:
@Table, This annotation specifies the primary table for the annotated entity.
@Id, Specifies database table primary key of a mapped entity.
@GeneratedValue, specification of generation strategies for the values of primary keys. it takes IDENTITY and AUTO as value.
@Column, Specifies database table mapped column for a persistent property or field.
@GeneratedType, Defines the types of primary key generation strategies.
@NotEmpty, The annotated element must not be null nor empty. Supported types are: CharSequence, Collection, Map and Array
@Email The string has to be a well-formed email address.

4. Create Repository for model

Adding javax and jpa to POM.xml

Adding TodoItemRepository.java

The query builder mechanism built into Spring Data repository infrastructure is useful for building constraining queries over entities of the repository

package com.example.demo.repositories;

import java.util.List;
import java.util.UUID;

import com.example.demo.models.TodoItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository("TodoItemRepository")
public interface TodoItemRepository extends JpaRepository<TodoItem, Long> 
{
    TodoItem findByItemId(Long itemId);
    List<TodoItem> findByListId(UUID listId);
}

5. Create Services

package com.example.demo.services;

import com.example.demo.models.*;
import com.example.demo.repositories.*;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID;
import java.util.Date;

@Service("TodoItemService")
public class TodoItemService {
    @Autowired
    private TodoItemRepository todoItemRepository;

    public TodoItem saveTodoItem(TodoItem item) {
        return todoItemRepository.save(item);
    }

    public TodoItem changeDoneStateForTodoItem(Long id) {
        TodoItem item = todoItemRepository.findByItemId(id);
        if (item != null) {
            item.setIsDone(!item.getIsDone());
            todoItemRepository.save(item);
            return item;
        }
        return null;
    }

    public Boolean deleteTodoItem(Long id) {
        TodoItem item = todoItemRepository.findById(id).orElse(null);
        if (item != null) {
            todoItemRepository.delete(item);
            return true;
        }
        return false;
    }

    public TodoItem editTodoItem(TodoItem editedItem)
    {
        TodoItem item = todoItemRepository.findById(editedItem.getItemId()).orElse(null);
        if (item != null) {
            item.setTaskName(editedItem.getTaskName());
            return todoItemRepository.save(item);
        }
        //Create new if we dont have.
        return todoItemRepository.save(item);
    }

    public List<TodoItem> getAllTodoItemsForListId(UUID listId) {
        return todoItemRepository.findByListId(listId);
    }

    public TodoItem getItem(Long id)
    {
        return todoItemRepository.findByItemId(id);
    }
}

6. Create Controller

This is REST Controller for CRUD todo items.

package com.example.demo.controllers;

import com.example.demo.models.*;
import com.example.demo.services.*;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import javax.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.ui.Model;

@RestController
@RequestMapping("/api/v1")
public class TodoItemController {
    @Autowired
    private TodoItemService itemService;

    @GetMapping("/item/{itemId}")
    public TodoItem getItem(@PathVariable Long itemId) {
        return itemService.getItem(itemId);
    }

    // Get todo list, based on listId
    @GetMapping("/list/{listId}")
    public List<TodoItem> getItem(@PathVariable UUID listId) {
        return itemService.getAllTodoItemsForListId(listId);
    }

    // New todo item
    @PostMapping(value = "/new")
    public ResponseEntity<TodoItem> newTodoItem(@RequestBody TodoItem item) {
        return ResponseEntity.ok(itemService.saveTodoItem(item));
    }

    // Edit todo item
    @PutMapping("/edit")
    public ResponseEntity<TodoItem> editTodoItem(@RequestBody TodoItem item) {
        return ResponseEntity.ok(itemService.editTodoItem(item));
    }

    // Delete todo item
    @DeleteMapping("/delete/{id}")
    public ResponseEntity<Boolean> deleteTodoItem(@PathVariable Long id) {
        return ResponseEntity.ok(itemService.deleteTodoItem(id));
    }

    // Change done state
    @PutMapping("/state/{id}")
    public ResponseEntity<TodoItem> changeDoneState(@PathVariable Long id) {
        return ResponseEntity.ok(itemService.changeDoneStateForTodoItem(id));
    }
}

7. Implement Client

We will use Thymeleaf for rendering client template and using Javascript/jQuery to call REST APIs.

In controllers folder, adding MainController.java

package com.example.demo.controllers;
import java.util.UUID;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.ui.Model;

@Controller
public class MainController {
    @RequestMapping(value = { "/", "/index" }, method = RequestMethod.GET)
    public String index(Model model) 
    {
        // When people visit site, create an UUID for a list and return it.
        UUID uuid = UUID.randomUUID();
        model.addAttribute("listId", uuid.toString()); 
        return "index";
    }
}

Then, adding css file in resource/static/css/style.css

h1 {
    color:#0000FF;
}
 
h2 {
    color:#FF0000;
}
 
table {
    border-collapse: collapse;
}
 
table th, table td {
    padding: 5px;
}

body {
    font-size: 18px;
    line-height: 1.58;
    background: #6699FF;
    background: -webkit-linear-gradient(to left, #336699, #228899);
    background: linear-gradient(to left, #336699, #228899); 
    color: #333;
  }
  
  h1 {
    font-size: 36px;
  }
  
  * {
    box-sizing: border-box;
  }
  
  i {
    vertical-align: middle;
    color: #626262;
  }
  
  input {
    border: 1px solid #E8E8E8;
  }
  
  .page-title {
    text-align: center;
  }
  
  .todo-content {
    max-width: 650px;
    width: 100%;
    margin: 0 auto;
    margin-top: 60px;
    background-color: #fff;
    padding: 15px; 
    box-shadow: 0 0 4px rgba(0,0,0,.14), 0 4px 8px rgba(0,0,0,.28);
  }
  
  .form-control {
    font-size: 16px;
    padding-left: 15px;
    outline: none;
    border: 1px solid #E8E8E8;
  }
  
  .form-control:focus {
    border: 1px solid #626262;
  }
  
  .todo-content .form-control {
    width: 100%;
    height: 50px;
  }
  
  .todo-content .todo-create {
    padding-bottom: 30px;
    border-bottom: 1px solid #e8e8e8;
  }
  
  .todo-content .alert-danger {
    padding-left: 15px;
    font-size: 14px;
    color: red;
    padding-top: 5px;
  }
  
  .todo-content ul {
    list-style: none;
    margin: 0;
    padding: 0;
    max-height: 450px;
    padding-left: 15px;
    padding-right: 15px;
    margin-left: -15px;
    margin-right: -15px;
    overflow-y: scroll;
  }
  
  .todo-content ul li {
      padding-top: 10px;
      padding-bottom: 10px;
      border-bottom: 1px solid #E8E8E8;
  }
  
  .todo-content ul li span {
    display: inline-block;
    vertical-align: middle;
  }
  
  .todo-content .todo-title {
    width: calc(100% - 160px);
    margin-left: 10px;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  
  .todo-content .todo-completed {
    display: inline-block;
    text-align: center;
    width:35px;
    height:35px;
    cursor: pointer;
  }
  
  .todo-content .todo-completed i {
    font-size: 20px;
  }
  
  .todo-content .todo-actions, .todo-content .edit-actions {
      float: right;
  }
  
  .todo-content .todo-actions i, .todo-content .edit-actions i {
      font-size: 17px;
  }
  
  .todo-content .todo-actions a, .todo-content .edit-actions a {
    display: inline-block;
    text-align: center;
    width: 35px;
    height: 35px;
    cursor: pointer;
  }
  
  .todo-content .todo-actions a:hover, .todo-content .edit-actions a:hover {
    background-color: #f4f4f4;
  }
  
  .todo-content .todo-edit input {
    width: calc(100% - 80px);
    height: 35px;
  }
  .todo-content .edit-actions {
    text-align: right;
  }
  
  .no-todos {
    text-align: center;
  }
  
  .toggle-completed-checkbox:before {
    content: 'check_box_outline_blank';
  }
  
  li.completed .toggle-completed-checkbox:before {
    content: 'check_box';
  }
  
  li.completed .todo-title {
    text-decoration: line-through;
    color: #757575;
  }
  
  li.completed i {
    color: #757575;
  }

Finally, we implement HTML file called index.html in resources/templates/index.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8" />
    <title>Create and share a check list just a second! Publist - Never miss a thing!</title>
    <link rel="stylesheet" type="text/css" th:href="@{/css/style.css}" />

    <!--For jquery calling REST API-->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <!--For material icon-->
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
</head>

<body>

    <div class="todo-content">
        <h1 class="page-title"> My Todo </h1>
        
        <div class="todo-create">
            <!--Create a text box and handle ENTER to create item-->
            <form action="#">
                <input type="text" id="taskNameTextField" class="form-control" onkeypress="handleEnterKey(event)"
                    placeholder="Input task name then tap Enter to add">
            </form>
        </div>

        <ul class="todo-list">
        </ul>

        <!-- First grab an UUID from backend -->
        <script th:inline="javascript">
            /*<![CDATA[*/
            var glistId = /*[[${listId}]]*/
        /*]]>*/
        </script>

        <script>
            function handleEnterKey(e) {
                if (e.keyCode === 13) {
                    e.preventDefault(); // Ensure it is only this code that run
                    var taskName = document.getElementById('taskNameTextField').value
                    // Clear input field!
                    $("#taskNameTextField").val('');

                    // Check if we are editing or not!
                    var isEditing = $("#taskNameTextField").attr("isEditing");

                    if (isEditing)
                    {
                        // clear editing flag.
                        $("#taskNameTextField").removeAttr("isEditing");
                        var itemId = $("#taskNameTextField").attr("editingItemId");
                        $("#taskNameTextField").removeAttr("editingItemId");
                        putEditTodoItem(itemId, taskName, glistId);
                    }
                    else
                    {
                        postNewTodoItem(taskName, glistId);
                    }
                }
            }

            // change Done state of an item
            function changeDoneState(ele) {
                var itemId = $(ele).attr("id"); // get the item id!
                $.ajax({
                    type: "PUT",
                    url: "http://localhost:4000/api/v1/state/" + itemId,
                    success: function (data) {
                        // Create new list item                        
                        var newListItem = $('<li/>')
                            .attr("id", "item" + data.itemId);
                        
                        if (data.isDone)
                        {
                            newListItem.addClass('completed')
                        }

                        var todoRow = createTodoRow(newListItem, data);
                        
                        // Replace the old one by the new one
                        var oldListItem = $("#item" + itemId);
                        oldListItem.replaceWith(newListItem);
                    },
                    error: function (data) {
                    }
                });
            }

            function putEditTodoItem(itemId, taskName, listId)
            {
                var newTodoItem = {
                    itemId: itemId,
                    taskName: taskName,
                    listId: listId
                };
                var requestJSON = JSON.stringify(newTodoItem);
                $.ajax({
                    type: "PUT",
                    url: "http://localhost:4000/api/v1/edit",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    data: requestJSON,
                    success: function (data) {
                        // Create new list item                        
                        var newListItem = $('<li/>')
                        .attr("id", "item" + data.itemId);
                        
                        if (data.isDone)
                        {
                            newListItem.addClass('completed')
                        }

                        createTodoRow(newListItem, data);
                        
                        // Replace the old one by the new one
                        var oldListItem = $("#item" + data.itemId);
                        oldListItem.replaceWith(newListItem);
                    },
                    error: function (data) {
                    }
                });
            }

            function postNewTodoItem(taskName, listId)
            {
                var newTodoItem = {
                    taskName: taskName,
                    listId: listId
                };
                var requestJSON = JSON.stringify(newTodoItem);
                $.ajax({
                    type: "POST",
                    url: "http://localhost:4000/api/v1/new",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    data: requestJSON,
                    success: function (data) {
                        var cList = $('ul.todo-list');
                        var li = $('<li/>')
                            .attr("id", "item" + data.itemId)
                            .appendTo(cList);

                        createTodoRow(li, data);
                    },
                    error: function (data) {
                    }
                });
            }

            function deleteTodoItem(ele) {
                var itemId = $(ele).attr("id"); // get the item id!
                $.ajax({
                    type: "DELETE",
                    url: "http://localhost:4000/api/v1/delete/" + itemId,
                    success: function (data) {
                        var oldItem = $("#item" + itemId);
                        cuteHide(oldItem);
                        oldItem.remove();
                    },
                    error: function (data) {
                    }
                });
            }
            
            function editTodoItem(ele) 
            {
                // first get item id
                var itemId = $(ele).attr("id");
                // then get list item we created before.
                var listItem =  $("#item" + itemId);
                var titleSpan = listItem.find(".todo-title");

                // set the text field
                $("#taskNameTextField").val(titleSpan.text());
                // set the attribute that we are editing!
                $("#taskNameTextField").attr("isEditing", true);
                $("#taskNameTextField").attr("editingItemId", itemId);
            }

            function createTodoRow(parent, data)
            {
                var todoRow = $('<div/>')
                    .addClass('todo-row')
                    .appendTo(parent)

                // Check BOX
                var checkBoxAttr = $('<a/>')
                    .attr("id", data.itemId) // to know item id!
                    .attr("onclick", "changeDoneState(this)")
                    .addClass('todo-completed')
                    .appendTo(todoRow);

                var checkBoxIcon = $('<i/>')
                    .addClass('material-icons toggle-completed-checkbox')
                    .appendTo(checkBoxAttr);

                // Task Name
                var todoTitle = $('<span/>')
                    .addClass('todo-title')
                    .text(data.taskName)
                    .appendTo(todoRow);

                // Actions
                var todoActions = $('<span/>')
                    .addClass('todo-actions')
                    .appendTo(todoRow)

                // Edit icon
                var editAttr = $('<a/>')
                    .attr("id", data.itemId) // to know item id!
                    .attr("onclick", "editTodoItem(this)")
                    .appendTo(todoActions);

                var editIcon = $('<i/>')
                    .addClass('material-icons')
                    .text('edit')
                    .appendTo(editAttr);

                // Delete icon
                var deleteAttr = $('<a/>')
                    .attr("id", data.itemId) // to know item id!
                    .attr("onclick", "deleteTodoItem(this)")
                    .appendTo(todoActions);

                var deleteIcon = $('<i/>')
                    .addClass('material-icons')
                    .text('delete')
                    .appendTo(deleteAttr);
            }
            // For animation when deleting
            function cuteHide(el) {
                    el.animate({ opacity: '0' }, 300, function () {
                        el.animate({ height: '0px' }, 300, function () {
                            el.remove();
                        });
                    });
                }
        </script>

</body>

</html>

Good luck!