Spring Boot Todo Web Application with MySQL Extension

Great. Let’s upgrade the Todo application into a proper Spring Boot CRUD project with:

  1. Add Todo
  2. View Todo
  3. Edit Todo
  4. Delete Todo
  5. Mark Complete/Incomplete
  6. Validation
  7. Bootstrap UI
  8. REST API
  9. MySQL persistence

We can build it incrementally so you understand Controller → Service → Repository → MySQL rather than just copying code.

Below is a complete Spring Boot Todo CRUD application you can import into Eclipse and run with MySQL.

It includes Web UI + Bootstrap + CRUD + validation + REST API + MySQL.

Project structure

todo/
├── pom.xml
└── src/
    └── main/
        ├── java/com/example/todo/
        │   ├── TodoApplication.java
        │   ├── controller/
        │   │   ├── TodoController.java
        │   │   └── TodoRestController.java
        │   ├── model/
        │   │   └── Todo.java
        │   ├── repository/
        │   │   └── TodoRepository.java
        │   └── service/
        │       └── TodoService.java
        │
        └── resources/
            ├── templates/
            │   ├── index.html
            │   └── edit.html
            └── application.properties

1. MySQL setup

Run:

sudo systemctl start mysql

Then:

sudo mysql

Create the database:

CREATE DATABASE todo_db;

CREATE USER 'todo_user'@'localhost'
IDENTIFIED BY 'todo_password';

GRANT ALL PRIVILEGES ON todo_db.*
TO 'todo_user'@'localhost';

FLUSH PRIVILEGES;

EXIT;

2. pom.xml

Replace your pom.xml with:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="
         http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.6</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>todo</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>todo</name>
    <description>Spring Boot Todo Application</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>

        <!-- Spring Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- Spring Data JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!-- Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- MySQL -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- DevTools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

        </plugins>
    </build>

</project>

If your Spring Initializr generated a newer Spring Boot 3.x version, you can keep that version instead of 3.5.6.


3. Main application

File:

src/main/java/com/example/todo/TodoApplication.java
package com.example.todo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TodoApplication {

    public static void main(String[] args) {

        SpringApplication.run(TodoApplication.class, args);

    }
}

4. Todo Entity

Create:

model/Todo.java
package com.example.todo.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

@Entity
public class Todo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Title is required")
    @Size(min = 3, max = 100,
          message = "Title must be between 3 and 100 characters")
    private String title;

    private boolean completed;

    public Todo() {
    }

    public Todo(String title) {
        this.title = title;
        this.completed = false;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public boolean isCompleted() {
        return completed;
    }

    public void setCompleted(boolean completed) {
        this.completed = completed;
    }
}

5. Repository

File:

repository/TodoRepository.java
package com.example.todo.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.todo.model.Todo;

public interface TodoRepository extends JpaRepository<Todo, Long> {

}

That’s all the repository needs for basic CRUD.

Spring gives us:

findAll()
findById()
save()
deleteById()
existsById()
count()

automatically.


6. Service

File:

service/TodoService.java
package com.example.todo.service;

import java.util.List;

import org.springframework.stereotype.Service;

import com.example.todo.model.Todo;
import com.example.todo.repository.TodoRepository;

@Service
public class TodoService {

    private final TodoRepository todoRepository;

    public TodoService(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }

    // READ ALL
    public List<Todo> getAllTodos() {

        return todoRepository.findAll();

    }

    // READ ONE
    public Todo getTodoById(Long id) {

        return todoRepository.findById(id)
                .orElseThrow(() ->
                    new RuntimeException("Todo not found: " + id)
                );

    }

    // CREATE
    public Todo saveTodo(Todo todo) {

        return todoRepository.save(todo);

    }

    // UPDATE
    public Todo updateTodo(Long id, Todo todoDetails) {

        Todo todo = getTodoById(id);

        todo.setTitle(todoDetails.getTitle());
        todo.setCompleted(todoDetails.isCompleted());

        return todoRepository.save(todo);

    }

    // DELETE
    public void deleteTodo(Long id) {

        if (!todoRepository.existsById(id)) {

            throw new RuntimeException(
                "Todo not found: " + id
            );

        }

        todoRepository.deleteById(id);

    }

    // TOGGLE
    public void toggleTodo(Long id) {

        Todo todo = getTodoById(id);

        todo.setCompleted(!todo.isCompleted());

        todoRepository.save(todo);

    }
}

7. Web Controller

This controller handles the HTML web pages.

File:

controller/TodoController.java
package com.example.todo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import com.example.todo.model.Todo;
import com.example.todo.service.TodoService;

import jakarta.validation.Valid;

@Controller
public class TodoController {

    private final TodoService todoService;

    public TodoController(TodoService todoService) {
        this.todoService = todoService;
    }

    // HOME PAGE
    @GetMapping("/")
    public String home(Model model) {

        model.addAttribute(
            "todos",
            todoService.getAllTodos()
        );

        model.addAttribute(
            "todo",
            new Todo()
        );

        return "index";
    }

    // CREATE
    @PostMapping("/todos")
    public String addTodo(
            @Valid @ModelAttribute("todo") Todo todo,
            BindingResult result,
            Model model) {

        if (result.hasErrors()) {

            model.addAttribute(
                "todos",
                todoService.getAllTodos()
            );

            return "index";
        }

        todoService.saveTodo(todo);

        return "redirect:/";
    }

    // EDIT PAGE
    @GetMapping("/todos/edit/{id}")
    public String editTodo(
            @PathVariable Long id,
            Model model) {

        Todo todo = todoService.getTodoById(id);

        model.addAttribute("todo", todo);

        return "edit";
    }

    // UPDATE
    @PostMapping("/todos/update/{id}")
    public String updateTodo(
            @PathVariable Long id,
            @Valid @ModelAttribute("todo") Todo todo,
            BindingResult result) {

        if (result.hasErrors()) {

            return "edit";
        }

        todoService.updateTodo(id, todo);

        return "redirect:/";
    }

    // DELETE
    @GetMapping("/todos/delete/{id}")
    public String deleteTodo(
            @PathVariable Long id) {

        todoService.deleteTodo(id);

        return "redirect:/";
    }

    // TOGGLE
    @GetMapping("/todos/toggle/{id}")
    public String toggleTodo(
            @PathVariable Long id) {

        todoService.toggleTodo(id);

        return "redirect:/";
    }
}

8. REST Controller

Now we also expose a REST API.

Create:

controller/TodoRestController.java
package com.example.todo.controller;

import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import com.example.todo.model.Todo;
import com.example.todo.service.TodoService;

@RestController
@RequestMapping("/api/todos")
public class TodoRestController {

    private final TodoService todoService;

    public TodoRestController(TodoService todoService) {
        this.todoService = todoService;
    }

    // GET ALL
    @GetMapping
    public List<Todo> getAllTodos() {

        return todoService.getAllTodos();

    }

    // GET ONE
    @GetMapping("/{id}")
    public ResponseEntity<Todo> getTodo(
            @PathVariable Long id) {

        try {

            return ResponseEntity.ok(
                todoService.getTodoById(id)
            );

        } catch (RuntimeException e) {

            return ResponseEntity.notFound().build();

        }
    }

    // CREATE
    @PostMapping
    public ResponseEntity<Todo> createTodo(
            @RequestBody Todo todo) {

        Todo savedTodo =
                todoService.saveTodo(todo);

        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(savedTodo);
    }

    // UPDATE
    @PutMapping("/{id}")
    public ResponseEntity<Todo> updateTodo(
            @PathVariable Long id,
            @RequestBody Todo todo) {

        try {

            Todo updatedTodo =
                    todoService.updateTodo(id, todo);

            return ResponseEntity.ok(updatedTodo);

        } catch (RuntimeException e) {

            return ResponseEntity.notFound().build();

        }
    }

    // DELETE
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteTodo(
            @PathVariable Long id) {

        try {

            todoService.deleteTodo(id);

            return ResponseEntity.noContent().build();

        } catch (RuntimeException e) {

            return ResponseEntity.notFound().build();

        }
    }

    // TOGGLE
    @PatchMapping("/{id}/toggle")
    public ResponseEntity<Todo> toggleTodo(
            @PathVariable Long id) {

        try {

            todoService.toggleTodo(id);

            return ResponseEntity.ok(
                todoService.getTodoById(id)
            );

        } catch (RuntimeException e) {

            return ResponseEntity.notFound().build();

        }
    }
}

9. application.properties

File:

src/main/resources/application.properties
spring.application.name=todo

# MySQL
spring.datasource.url=jdbc:mysql://localhost:3306/todo_db
spring.datasource.username=todo_user
spring.datasource.password=todo_password

# JPA / Hibernate
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Thymeleaf
spring.thymeleaf.cache=false

# Server
server.port=8080

10. Main HTML page

Create:

src/main/resources/templates/index.html
<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org">

<head>

    <meta charset="UTF-8">

    <meta name="viewport"
          content="width=device-width, initial-scale=1">

    <title>Todo Application</title>

    <link
        href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
        rel="stylesheet">

</head>

<body class="bg-light">

<div class="container mt-5">

    <div class="row justify-content-center">

        <div class="col-md-8">

            <div class="card shadow">

                <div class="card-header bg-primary text-white">

                    <h2 class="text-center mb-0">
                        Todo Application
                    </h2>

                </div>

                <div class="card-body">

                    <!-- ADD TODO -->

                    <form
                        th:action="@{/todos}"
                        th:object="${todo}"
                        method="post"
                        class="mb-4">

                        <div class="input-group">

                            <input
                                type="text"
                                th:field="*{title}"
                                class="form-control"
                                placeholder="Enter todo">

                            <button
                                type="submit"
                                class="btn btn-primary">

                                Add Todo

                            </button>

                        </div>

                        <div
                            th:if="${#fields.hasErrors('title')}"
                            th:errors="*{title}"
                            class="text-danger mt-2">
                        </div>

                    </form>


                    <!-- TODO TABLE -->

                    <table class="table table-hover">

                        <thead class="table-dark">

                        <tr>

                            <th>ID</th>

                            <th>Todo</th>

                            <th>Status</th>

                            <th>Actions</th>

                        </tr>

                        </thead>

                        <tbody>

                        <tr th:each="todo : ${todos}">

                            <td th:text="${todo.id}">
                                1
                            </td>

                            <td>

                                <span
                                    th:text="${todo.title}"
                                    th:classappend="${todo.completed}
                                    ? 'text-decoration-line-through text-muted'
                                    : ''">

                                    Learn Spring Boot

                                </span>

                            </td>

                            <td>

                                <span
                                    th:if="${todo.completed}"
                                    class="badge bg-success">

                                    Completed

                                </span>

                                <span
                                    th:unless="${todo.completed}"
                                    class="badge bg-warning text-dark">

                                    Pending

                                </span>

                            </td>

                            <td>

                                <a
                                    th:href="@{/todos/toggle/{id}(id=${todo.id})}"
                                    class="btn btn-sm btn-success">

                                    Toggle

                                </a>

                                <a
                                    th:href="@{/todos/edit/{id}(id=${todo.id})}"
                                    class="btn btn-sm btn-primary">

                                    Edit

                                </a>

                                <a
                                    th:href="@{/todos/delete/{id}(id=${todo.id})}"
                                    class="btn btn-sm btn-danger"
                                    onclick="return confirm('Delete this todo?');">

                                    Delete

                                </a>

                            </td>

                        </tr>

                        <tr th:if="${#lists.isEmpty(todos)}">

                            <td
                                colspan="4"
                                class="text-center text-muted">

                                No todos found.

                            </td>

                        </tr>

                        </tbody>

                    </table>

                </div>

            </div>

        </div>

    </div>

</div>

</body>

</html>

11. Edit page

Create:

src/main/resources/templates/edit.html
<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org">

<head>

    <meta charset="UTF-8">

    <meta name="viewport"
          content="width=device-width, initial-scale=1">

    <title>Edit Todo</title>

    <link
        href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
        rel="stylesheet">

</head>

<body class="bg-light">

<div class="container mt-5">

    <div class="row justify-content-center">

        <div class="col-md-6">

            <div class="card shadow">

                <div class="card-header bg-primary text-white">

                    <h3 class="text-center mb-0">
                        Edit Todo
                    </h3>

                </div>

                <div class="card-body">

                    <form
                        th:action="@{/todos/update/{id}(id=${todo.id})}"
                        th:object="${todo}"
                        method="post">

                        <div class="mb-3">

                            <label class="form-label">
                                Todo
                            </label>

                            <input
                                type="text"
                                th:field="*{title}"
                                class="form-control">

                            <div
                                th:if="${#fields.hasErrors('title')}"
                                th:errors="*{title}"
                                class="text-danger">
                            </div>

                        </div>

                        <div class="form-check mb-3">

                            <input
                                type="checkbox"
                                th:field="*{completed}"
                                class="form-check-input">

                            <label class="form-check-label">
                                Completed
                            </label>

                        </div>

                        <button
                            type="submit"
                            class="btn btn-primary">

                            Update

                        </button>

                        <a
                            href="/"
                            class="btn btn-secondary">

                            Cancel

                        </a>

                    </form>

                </div>

            </div>

        </div>

    </div>

</div>

</body>

</html>

12. Run the application

From the project directory:

cd todo

Run:

./mvnw spring-boot:run

Or in Eclipse:

Right click TodoApplication.java
        ↓
Run As
        ↓
Spring Boot App

Then open:

Todo Application


13. Test CRUD

Create

Enter:

Learn Java

Click Add Todo.

Read

The todo appears in the table.

Update

Click:

Edit

Change:

Learn Java

to:

Learn Java and Spring Boot

Click Update.

Complete

Click:

Toggle

The status changes:

Pending

to:

Completed

Delete

Click:

Delete

The record is removed from MySQL.


14. Test REST API

You can test the REST API using Postman, curl, or your browser.

Get all todos

curl http://localhost:8080/api/todos

Example:

[
    {
        "id": 1,
        "title": "Learn Java",
        "completed": false
    },
    {
        "id": 2,
        "title": "Learn Spring Boot",
        "completed": true
    }
]

Get one todo

curl http://localhost:8080/api/todos/1

Create todo

curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"Learn REST API","completed":false}' \
http://localhost:8080/api/todos

Update todo

curl -X PUT \
-H "Content-Type: application/json" \
-d '{"title":"Learn Spring Boot REST","completed":true}' \
http://localhost:8080/api/todos/1

Toggle todo

curl -X PATCH \
http://localhost:8080/api/todos/1/toggle

Delete todo

curl -X DELETE \
http://localhost:8080/api/todos/1

15. Check MySQL

mysql -u todo_user -p

Then:

USE todo_db;

Check tables:

SHOW TABLES;

Check todos:

SELECT * FROM todo;

You should get something like:

+----+-------------------------+-----------+
| id | title                   | completed |
+----+-------------------------+-----------+
|  1 | Learn Java              |         0 |
|  2 | Learn Spring Boot       |         1 |
|  3 | Learn REST API          |         0 |
+----+-------------------------+-----------+

16. How the application works

The important Spring architecture is:

                    Browser
                       |
                       v
              TodoController
                       |
                       v
                TodoService
                       |
                       v
              TodoRepository
                       |
                       v
                 JPA/Hibernate
                       |
                       v
                    MySQL

For REST:

Postman / curl / Frontend
          |
          v
  TodoRestController
          |
          v
     TodoService
          |
          v
   TodoRepository
          |
          v
       MySQL

Create

POST /todos
       ↓
Controller
       ↓
Service
       ↓
Repository
       ↓
INSERT
       ↓
MySQL

Read

GET /
   ↓
Controller
   ↓
Service
   ↓
Repository
   ↓
SELECT
   ↓
Thymeleaf
   ↓
HTML

Update

PUT /api/todos/1
       ↓
Controller
       ↓
Service
       ↓
Repository
       ↓
UPDATE
       ↓
MySQL

Delete

DELETE /api/todos/1
       ↓
Controller
       ↓
Service
       ↓
Repository
       ↓
DELETE
       ↓
MySQL

17. Final API table

MethodURLPurpose
GET/api/todosGet all
GET/api/todos/1Get one
POST/api/todosCreate
PUT/api/todos/1Update
PATCH/api/todos/1/toggleToggle
DELETE/api/todos/1Delete

And the web application:

MethodURLPurpose
GET/Todo home
POST/todosAdd
GET/todos/edit/1Edit page
POST/todos/update/1Update
GET/todos/toggle/1Toggle
GET/todos/delete/1Delete

This gives you a complete Spring Boot + Maven + MySQL + JPA/Hibernate + Thymeleaf + Bootstrap + REST CRUD project suitable for practicing in Eclipse on Ubuntu.

Leave a Reply

Your email address will not be published. Required fields are marked *