Implement a complete server-rendered todo list and understand every layer behind the form.
Java 21Spring Boot 4.1.1Maven 3.9+MongoDB Atlas
How to use this manual: Read Spring foundations and complete Atlas setup once. Then follow the numbered steps here in order. Every source file is included below with a copy button, its exact relative path, and an explanation. Downloading the bundle is an alternative to creating the files by hand.
1. Understand the application
This app provides a shared todo list. You can add a task, list all tasks, edit the title and completed status, and delete a task. Documents persist in Atlas database todo_mvc. The browser receives server-rendered HTML; no React or Postman is required for this lab.
HTML form → TodoController → TodoService → TodoRepository → Atlas
Spring Framework provides dependency injection and MVC. Spring Boot starts and configures the application. Thymeleaf renders templates. Spring Data MongoDB maps and stores documents. Spring Security supplies CSRF protection on the forms.
2. Create the project and folders
Choose a working folder, such as C:\training, in which you can create projects. In the downloaded bundle, open projects/todo-mvc directly in your IDE as a Maven project. To build from this manual instead, create a folder called todo-mvc, then create every file shown in the next step at its exact path.
PowerShell / Bash · manual creation only
mkdir todo-mvc
cd todo-mvc
A directory separator may be / or \ on Windows. Match Java class names and filenames exactly. Do not place Java source files in the project root or inside resources. The first line of each Java file is package com.example.todo;.
Import or reload Maven after saving pom.xml. The package root is src/main/java/com/example/todo. Tests go under src/test/java/com/example/todo. Resources go under src/main/resources. No Lombok, code-generation step, or omitted helper class is required.
3. Write the complete source files
Create the files below in the order shown. Save each whole code block as the indicated file. Test files are also included; you can copy them after the application files.
The parent pins Spring Boot 4.1.1 and manages compatible dependency versions. Web MVC supplies DispatcherServlet and embedded Tomcat. MongoDB supplies Spring Data repositories and the driver; validation supplies Jakarta Bean Validation. Java 21 is the example baseline. The Boot Maven plugin builds an executable JAR.
This main method starts the Spring container and the embedded web server. @SpringBootApplication combines configuration, auto-configuration, and component scanning. Keep every application class below com.example.todo so Spring discovers it.
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);
}
}
Boot 4 uses spring.mongodb.uri and spring.mongodb.database. Environment placeholders keep credentials out of source. Each lab has its own database and port. Automatic index creation is enabled for the example database; personal users require a unique email index. In a deployed system, create and review indexes through a migration process.
Build output and local secret files must stay out of Git. An .env file is not automatically loaded by Spring Boot; use the documented terminal environment variables.
@Document maps this Java class to the todos collection. @Id maps id to MongoDB’s _id. A String id can represent the generated ObjectId. The no-argument constructor and accessors support mapping and serialization. completed defaults to false; title is stored as a string. This is a document model, not a relational @Entity.
Java
package com.example.todo;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document("todos")
public class Todo {
@Id private String id;
private String title;
private boolean completed;
public Todo() {}
public Todo(String title, boolean completed) { this.title=title; this.completed=completed; }
public String getId() { return id; }
public void setId(String 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; }
}
Spring creates a repository implementation at runtime. Todo is the document type and String is the identifier type. Inherited methods include findAll, findById, save, and deleteById. save inserts a new object with no id or replaces an existing document. There is no SQL or JDBC connection here.
Constructor injection makes dependencies explicit and supports tests with a mock repository. The service owns business operations; controllers only translate HTTP. get throws 404 when missing. update reads first, so an unknown id cannot become an accidental insert. strip removes surrounding whitespace. Sorting makes the displayed task order stable. Reading then saving is simple but concurrent edits use last-write-wins; add @Version as an extension.
Java
package com.example.todo;
import java.util.Comparator;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import static org.springframework.http.HttpStatus.NOT_FOUND;
@Service
public class TodoService {
private final TodoRepository repository;
public TodoService(TodoRepository repository) { this.repository=repository; }
public List<Todo> list() {
return repository.findAll().stream().sorted(Comparator.comparing(Todo::getId)).toList();
}
public Todo get(String id) {
return repository.findById(id).orElseThrow(() -> new ResponseStatusException(NOT_FOUND,"Todo not found"));
}
public Todo create(String title, boolean completed) {
return repository.save(new Todo(title.strip(),completed));
}
public Todo update(String id, String title, boolean completed) {
Todo todo=get(id); todo.setTitle(title.strip()); todo.setCompleted(completed);
return repository.save(todo);
}
public void delete(String id) { Todo todo=get(id); repository.deleteById(todo.getId()); }
}
The form object contains only editable fields. Binding a separate form prevents visitors from setting a database id. @NotBlank rejects null, empty, or whitespace titles; @Size bounds storage and layout. Server validation still applies when someone bypasses HTML validation.
Java
package com.example.todo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class TodoForm {
@NotBlank(message="Enter a title")
@Size(max=120, message="Use at most 120 characters")
private String title;
private boolean completed;
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; }
}
This first lab is a shared todo list. All pages are public, while Spring Security keeps CSRF protection for HTML forms. Thymeleaf’s th:action forms receive a hidden CSRF field automatically. No login is configured for this lab. GET requests display pages; POST requests change data.
Unlike a Servlet’s doGet/doPost, annotated methods declare routes. @Controller returns a view name resolved under templates/. Model is the data passed to the template. @ModelAttribute binds form fields. BindingResult must immediately follow the validated form argument. On validation failure the same form is rendered with errors. After success, redirect:/todos implements Post/Redirect/Get and prevents refreshing from resubmitting a successful form.
th:each repeats a row, th:if shows an empty state, and th:text escapes user content. @{...} builds context-aware URLs. The delete operation is a POST form rather than a link, with the automatic CSRF token. Thymeleaf executes on the server; the browser receives ordinary HTML.
th:object selects the form bean; *{title} selects one of its properties. th:field generates name, value, and checkbox binding support. th:errors prints server validation errors. The controller supplies action for create versus update, allowing one reusable view.
These tests exercise useful service behavior: an unknown update must not insert data, and saved titles are trimmed. They use mocks and never connect to Atlas.
On a successful POST, the browser follows the redirect and sends a separate GET. Refreshing that resulting GET does not repeat the insertion. On an invalid form, the controller returns the view immediately with BindingResult errors, retaining the user’s fields.
Model attributes must match the template names: todos, todoForm, and action. Returning "form" resolves templates/form.html, not a Java class or a URL. In Thymeleaf, ${...} evaluates a model expression; *{...} evaluates relative to the selected form object; @{...} constructs URLs.
A checkbox that is unchecked ordinarily submits no value. th:field supplies binding support so completed becomes false. Spring Security adds a CSRF token to forms using th:action. View the generated form in browser developer tools to see _csrf. Manually posting without the session’s token should receive 403.
5. Connect to Atlas and start
From the todo-mvc directory, set your actual Atlas connection string. The properties file selects the lab’s database separately. Never paste a real connection string into source control or a presentation.
Windows PowerShell
# Run in the SAME PowerShell terminal that will start Spring Boot.
$env:MONGODB_URI = 'mongodb+srv://YOUR_DB_USER:YOUR_ENCODED_PASSWORD@YOUR_CLUSTER.mongodb.net/?retryWrites=true&w=majority&authSource=admin'
# Replace the three placeholders with the Atlas Drivers connection details.
# Do not copy the example host literally.
mvn test
mvn spring-boot:run
macOS / Linux Bash
export MONGODB_URI='mongodb+srv://YOUR_DB_USER:YOUR_ENCODED_PASSWORD@YOUR_CLUSTER.mongodb.net/?retryWrites=true&w=majority&authSource=admin'
mvn test
mvn spring-boot:run
Tests should finish with BUILD SUCCESS. Starting the app leaves the terminal occupied; keep it running and use a separate terminal or browser for requests. Look for the “Started TodoApplication” message and Tomcat on port 8080. The first database operation also verifies Atlas access; a successful server startup alone does not prove that reads and writes work.
To stop, press Ctrl+C. To package and run without Maven’s development command:
The same environment variables must exist when running the JAR. Run only one instance per port. To change the port temporarily in PowerShell, set $env:SERVER_PORT = '8090' before starting. To remove a terminal variable later, use Remove-Item Env:MONGODB_URI.
Open http://localhost:8080/todos. You should see “Classroom todos” and the empty-state message. Use the application URL, not templates/list.html on disk.
6. Execute the acceptance checklist
Click Add a todo. Enter “Prepare Spring class” and leave Completed unchecked. Save. Expect the list with status Pending.
Add “Create Atlas cluster”. Expect two independent tasks.
Edit the first task, rename it “Deliver Spring class”, and check Completed. Save. Expect Done.
Submit a whitespace-only title. Browser required may allow spaces, but server @NotBlank rejects them. Expect an error and no new task.
Enter a title longer than 120 characters using a crafted request or modified HTML maxlength. Expect the server validation error.
Add the text <script>alert(1)</script> as a title. Expect literal text; th:text escapes HTML.
Delete one task. Expect it to disappear and remain absent after refresh.
Stop and restart the app. Expect remaining tasks to persist. Verify documents in Atlas.
Open a deliberately invalid edit id. Expect HTTP 404.
Refresh after a successful create. Expect no duplicate insertion.
The supplied unit tests verify trimming and missing-update behavior. The checklist verifies browser rendering, form binding, CSRF integration, and real persistence.
7. Explain and extend the implementation
Ask learners to trace one create request and identify the layer responsible for each operation. The controller handles binding; Bean Validation checks constraints; the service trims the title and invokes the repository; the repository performs the document write; the controller chooses the next response.
Suggested extension: add a description field with a 500-character limit. Update the document, form DTO, service, controller mapping, and both templates. Then add a completed-only filter using a derived repository query. Make learners explain each layer they changed.
The app loads the full collection for this small sample dataset. For large lists, add pagination, indexes for filter fields, and optimistic locking for concurrent edits. Trace the basic CRUD flow before adding these features.
8. Troubleshooting
Symptom
Cause and fix
mvn is not recognized
Install Maven and add its bin directory to PATH. Open a new terminal. Verify mvn -version.
Cannot resolve placeholder MONGODB_URI
Set the environment variable in the terminal that starts the app. IDE launches need an environment variable in their Run Configuration. Spring does not automatically read a .env file.
Connection points to localhost
Use spring.mongodb.uri for this Boot 4 project; a Boot 3 spring.data.mongodb.uri property is not the property used here. Check spelling and active configuration.
Authentication failed / code 18
Use the Atlas database user, not your Atlas website login. Reset its password if needed. Percent-encode URI reserved characters. Verify authSource=admin.
Server selection timeout
Add your current public IP in Atlas Network Access; verify firewall access, active cluster, hostname, and outbound network connectivity. IP addresses can change on Wi-Fi or VPN.
UnknownHostException / SRV lookup failed
Re-copy the actual hostname. SRV requires DNS support; corporate DNS may block it. Use the standard connection string provided by Atlas if your environment cannot resolve SRV.
Address already in use
Stop the old process with Ctrl+C or override SERVER_PORT. If changing the personal API port, change React BASE and the Postman baseUrl too.
HTTP 400
Check the JSON syntax, Content-Type, title, and completed. Inspect details in the JSON error. In the MVC lab, inspect the form validation message.
HTTP 404
Check route and id. In the personal app, a task owned by another user also returns 404.
Browser fails but Postman works
Postman does not enforce browser CORS. Use the documented frontend origin exactly and inspect the browser Network panel.
Dependency download fails
Check proxy settings, Maven settings.xml, and repository reachability. An offline build works only after dependencies have already been downloaded.
If a template cannot be found, check src/main/resources/templates and its view name. If a form returns 403, keep th:action and Spring Security’s CSRF integration; do not disable protection to hide the problem. If CSS returns 404, use /style.css while storing the file under static/.