Todo App with Spring Boot MVC and Thymeleaf.

Step 1: Project Setup

We start by creating a Spring Boot project. You can use:

Spring Initializr (https://start.spring.io/)
Or directly in IntelliJ/Eclipse with “Spring Initializr project”

Choose dependencies:

Spring Web → to build M…


This content originally appeared on DEV Community and was authored by sadiul hakim

Step 1: Project Setup

We start by creating a Spring Boot project. You can use:

Choose dependencies:

  • Spring Web → to build MVC controllers
  • Thymeleaf → to render HTML templates
  • Spring Data JPA → to interact with database
  • H2 Database → in-memory DB for development
  • Spring Security → for login/logout

pom.xml will have (important parts):

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

    <!-- JPA + H2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

Step 2: Configure Application

We want H2 database in memory, and to see SQL queries.

src/main/resources/application.properties:

# Enable H2 console (useful for debugging)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# H2 database in memory
spring.datasource.url=jdbc:h2:mem:todoapp
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# Hibernate (JPA) config
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

Now our app will use an in-memory DB (todoapp) and automatically create tables from entities.

Step 3: Create the Todo Model

We need a table todo with columns: id, title, completed, username.

Todo.java

package com.example.todoapp.model;

import jakarta.persistence.*;
import lombok.Data;

@Entity
@Data
public class Todo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // Auto increment ID
    private Long id;

    private String title;

    private boolean completed = false;

    // Every todo belongs to a specific user
    private String username;
}
  • @Entity → tells JPA this is a table.
  • @Id → primary key.
  • username → ensures each user only sees their own todos.

Step 4: Repository Layer

We need a repository to interact with DB.

TodoRepository.java

package com.example.todoapp.repository;

import com.example.todoapp.model.Todo;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface TodoRepository extends JpaRepository<Todo, Long> {
    // Custom query: fetch only todos belonging to logged-in user
    List<Todo> findByUsername(String username);
}

This gives us CRUD methods like save, findById, deleteById for free.

Step 5: Security Setup (Login Form)

We don’t want open access. Let’s use Spring Security with in-memory users (simplest).

SecurityConfig.java

package com.example.todoapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/css/**").permitAll() // Bootstrap CSS allowed
                .anyRequest().authenticated() // everything else needs login
            )
            .formLogin(login -> login
                .loginPage("/login").permitAll() // custom login page
                .defaultSuccessUrl("/todos", true) // redirect after login
            )
            .logout(logout -> logout.permitAll());

        return http.build();
    }

    // In-memory users (for demo)
    @Bean
    public UserDetailsService users() {
        UserDetails user1 = User.withUsername("john")
                .password("1234")
                .roles("USER")
                .build();

        UserDetails user2 = User.withUsername("jane")
                .password("1234")
                .roles("USER")
                .build();

        return new InMemoryUserDetailsManager(user1, user2);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // ⚠ Not safe for real apps. Only for demo.
        return NoOpPasswordEncoder.getInstance();
    }
}

Now app has:

  • Login page at /login
  • Users:

    • john / 1234
    • jane / 1234

Step 6: Controller

This is where user actions are handled:

  • Show todos
  • Add new todo
  • Toggle status
  • Delete todo

TodoController.java

package com.example.todoapp.controller;

import com.example.todoapp.model.Todo;
import com.example.todoapp.repository.TodoRepository;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("/todos")
public class TodoController {

    private final TodoRepository repo;

    public TodoController(TodoRepository repo) {
        this.repo = repo;
    }

    // Show all todos of logged-in user
    @GetMapping
    public String listTodos(Model model, Authentication auth) {
        String username = auth.getName();
        model.addAttribute("todos", repo.findByUsername(username));
        model.addAttribute("newTodo", new Todo());
        return "todos"; // renders todos.html
    }

    // Add new todo
    @PostMapping
    public String addTodo(@ModelAttribute Todo todo, Authentication auth) {
        todo.setUsername(auth.getName());
        repo.save(todo);
        return "redirect:/todos";
    }

    // Toggle completion
    @PostMapping("/{id}/toggle")
    public String toggleTodo(@PathVariable Long id) {
        Todo todo = repo.findById(id).orElseThrow();
        todo.setCompleted(!todo.isCompleted());
        repo.save(todo);
        return "redirect:/todos";
    }

    // Delete todo
    @PostMapping("/{id}/delete")
    public String deleteTodo(@PathVariable Long id) {
        repo.deleteById(id);
        return "redirect:/todos";
    }
}

Step 7: Frontend (Thymeleaf + Bootstrap)

login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
    <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/>
</head>
<body class="container mt-5">
<div class="row justify-content-center">
    <div class="col-md-4">
        <h2 class="text-center">Login</h2>
        <form th:action="@{/login}" method="post">
            <div class="mb-3">
                <label>Username</label>
                <input class="form-control" type="text" name="username"/>
            </div>
            <div class="mb-3">
                <label>Password</label>
                <input class="form-control" type="password" name="password"/>
            </div>
            <button class="btn btn-primary w-100">Login</button>
        </form>
    </div>
</div>
</body>
</html>

todos.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Todos</title>
    <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/>
</head>
<body class="container mt-5">

<h2>Your Todos</h2>

<!-- Add new todo -->
<form th:action="@{/todos}" method="post" class="row g-3 mb-4">
    <div class="col-md-8">
        <input class="form-control" type="text" name="title" placeholder="New todo"/>
    </div>
    <div class="col-md-4">
        <button class="btn btn-success w-100">Add</button>
    </div>
</form>

<!-- List all todos -->
<table class="table table-bordered">
    <thead>
    <tr>
        <th>Title</th>
        <th>Status</th>
        <th>Actions</th>
    </tr>
    </thead>
    <tbody>
    <tr th:each="todo : ${todos}">
        <td th:text="${todo.title}"></td>
        <td>
            <span th:text="${todo.completed} ? '✅ Done' : '❌ Not done'"></span>
        </td>
        <td>
            <form th:action="@{/todos/{id}/toggle(id=${todo.id})}" method="post" style="display:inline">
                <button class="btn btn-warning btn-sm">Toggle</button>
            </form>
            <form th:action="@{/todos/{id}/delete(id=${todo.id})}" method="post" style="display:inline">
                <button class="btn btn-danger btn-sm">Delete</button>
            </form>
        </td>
    </tr>
    </tbody>
</table>

<a th:href="@{/logout}" class="btn btn-secondary">Logout</a>

</body>
</html>

Step 8: Run & Test

  1. Run with mvn spring-boot:run
  2. Open http://localhost:8080/login
  3. Login with:
  • john / 1234
  • jane / 1234
    1. Add todos, toggle, delete. Each user sees their own list.

Summary of Flow

  1. User logs in (/login) → Spring Security checks credentials.
  2. After login → redirect to /todos.
  3. TodoController fetches todos of logged-in user.
  4. Thymeleaf displays todos in todos.html.
  5. Bootstrap styles forms + tables.
  6. H2 stores todos in memory (resets when app restarts).


This content originally appeared on DEV Community and was authored by sadiul hakim


Print Share Comment Cite Upload Translate Updates
APA

sadiul hakim | Sciencx (2025-09-16T11:19:34+00:00) Todo App with Spring Boot MVC and Thymeleaf.. Retrieved from https://www.scien.cx/2025/09/16/todo-app-with-spring-boot-mvc-and-thymeleaf/

MLA
" » Todo App with Spring Boot MVC and Thymeleaf.." sadiul hakim | Sciencx - Tuesday September 16, 2025, https://www.scien.cx/2025/09/16/todo-app-with-spring-boot-mvc-and-thymeleaf/
HARVARD
sadiul hakim | Sciencx Tuesday September 16, 2025 » Todo App with Spring Boot MVC and Thymeleaf.., viewed ,<https://www.scien.cx/2025/09/16/todo-app-with-spring-boot-mvc-and-thymeleaf/>
VANCOUVER
sadiul hakim | Sciencx - » Todo App with Spring Boot MVC and Thymeleaf.. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/09/16/todo-app-with-spring-boot-mvc-and-thymeleaf/
CHICAGO
" » Todo App with Spring Boot MVC and Thymeleaf.." sadiul hakim | Sciencx - Accessed . https://www.scien.cx/2025/09/16/todo-app-with-spring-boot-mvc-and-thymeleaf/
IEEE
" » Todo App with Spring Boot MVC and Thymeleaf.." sadiul hakim | Sciencx [Online]. Available: https://www.scien.cx/2025/09/16/todo-app-with-spring-boot-mvc-and-thymeleaf/. [Accessed: ]
rf:citation
» Todo App with Spring Boot MVC and Thymeleaf. | sadiul hakim | Sciencx | https://www.scien.cx/2025/09/16/todo-app-with-spring-boot-mvc-and-thymeleaf/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.