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 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
- Run with
mvn spring-boot:run
- Open http://localhost:8080/login
- Login with:
john / 1234
-
jane / 1234
- Add todos, toggle, delete. Each user sees their own list.
Summary of Flow
- User logs in (
/login
) → Spring Security checks credentials. - After login → redirect to
/todos
. -
TodoController
fetches todos of logged-in user. - Thymeleaf displays todos in
todos.html
. - Bootstrap styles forms + tables.
- H2 stores todos in memory (resets when app restarts).
Top comments (0)