Great. Let’s upgrade the Todo application into a proper Spring Boot CRUD project with:
- Add Todo
- View Todo
- Edit Todo
- Delete Todo
- Mark Complete/Incomplete
- Validation
- Bootstrap UI
- REST API
- 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:
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
| Method | URL | Purpose |
|---|---|---|
| GET | /api/todos | Get all |
| GET | /api/todos/1 | Get one |
| POST | /api/todos | Create |
| PUT | /api/todos/1 | Update |
| PATCH | /api/todos/1/toggle | Toggle |
| DELETE | /api/todos/1 | Delete |
And the web application:
| Method | URL | Purpose |
|---|---|---|
| GET | / | Todo home |
| POST | /todos | Add |
| GET | /todos/edit/1 | Edit page |
| POST | /todos/update/1 | Update |
| GET | /todos/toggle/1 | Toggle |
| GET | /todos/delete/1 | Delete |
This gives you a complete Spring Boot + Maven + MySQL + JPA/Hibernate + Thymeleaf + Bootstrap + REST CRUD project suitable for practicing in Eclipse on Ubuntu.