Building a ToDo List CRUD API with the Java Spring Boot Framework

Introduction
Creating a fully functional CRUD (Create, Read, Update, Delete) REST API is a crucial skill for software engineers. In this tutorial, we’ll walk through the process of creating a ToDo list CRUD API using the Java Spring Boot framework. By the end of this tutorial, you will have a solid grasp of constructing APIs with Spring Boot, enabling you to effectively manage and manipulate data. The completed source code for this example can be found in the following Github Repository.
Prerequisites
Before starting this tutorial, make sure you have the following tools installed on your system. This article also assumes you have a basic understanding of Java programming.
- Java Development Kit (JDK)
- An Integrated Development Environment (IDE) of your choosing like IntelliJ IDEA or Eclipse
- Postman or a similar API testing tool
Step 1: Setting Up Our Project
- Visit the Spring Initializr at https://start.spring.io/. This is the web based setup wizard that will help us set up the foundation of our Spring Boot project.
- Choose Maven or Gradle as the build tool and Java as the programming Language. In our example we will select Maven as our build tool.

3. Add dependencies: Spring Web, Spring Data JPA, H2 Database.
- Spring Web: Enables us to build and manage our API’s web layer, handling incoming requests and sending responses
- Spring Data JPA: Gives us simplified access to a relational database, making it easier to manage, query, and manipulate our data.
- H2 Database: An in-memory database that facilitates testing and development, allowing us to interact with data without setting up an external database initially.

4. Fill in your Project Metadata:
- Group: This represents the base package for your project’s classes. It’s usually in reverse domain format (e.g.,
com.mycompany). In this example our group will be the defaultcom.example - Artifact: This is the name of your application or project. In this example our artifact will be
todolist - Name: Provide a descriptive name for your project. The name will also be
todolist - Description: Briefly describe your project’s purpose. In this example our description will be
To-Do List API’s - Package Name: This is automatically generated based on your Group and Artifact values. In this example our package name will be
com.example.todolist. - Packaging: Choose
JarorWarbased on your deployment needs. In this example we choseJar. - Java: Select the version of Java you want to use (e.g., 11, 16). In this example we are working with Java version 17.

5. Click the Generate button to create your project.
6. Unzip the contents of the generated project and import the project into the IDE of your choosing. In our example we are using the SpringToolSuite IDE powered by Eclipse. In order to import the project into SpringToolSuite you can do the following
- File -> Import Projects From File System…
- In the Import Source field, include the directory of your unzipped project then click Finish
Step 2: Designing the Data Model
Create the ToDo Entity and Import Dependencies
In your preferred IDE create a Java class called Todo in your default package. This class will serve as our entity representing a ToDo item.
package com.example.todolist;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Todo {}Define Fields
Inside the Todo class, define fields that represent the properties of a ToDo item. For example, add fields like id, title, and completed.
@Entity
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private boolean completed;
}Notice that the id field is annotated with @Id to mark it as the primary key.
The @GeneratedValue(strategy = GenerationType.IDENTITY) indicates that the id will be automatically generated when a new ToDo item is created.
Create the Constructors
Next we will create our constructors for our Todo entity. We’ll have two constructors: the default constructor and the parameterized constructor.
The default constructor doesn’t take any arguments and can be useful when creating instances without specific initial values. It’s created automatically if you don’t define any constructors.
The parameterized constructor takes title and completed as arguments. It allows us to provide these values during object creation. The this keyword is used to refer to the instance being created and set its attributes accordingly.
public Todo() {
}
public Todo(String title, boolean completed) {
this.title = title;
this.completed = completed;
}Creating the Getters and Setters
After defining our constructors, we will next create our getter and setter methods. Getters and setter methods allow controlled access to the attributes of a class. In our Todo class, we'll implement these methods to interact with the id, title, and completed fields.
A getter method retrieves the value of an attribute, and a setter method assigns a value to an attribute.
By manually creating these methods, we encapsulate access to the attributes, ensuring that they are accessed and modified consistently. Below you can see the completed Todo entity with its attributes, constructors, getter, and setter methods.
package com.example.todolist;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private boolean completed;
// Constructors
public Todo() {
}
public Todo(String title, boolean completed) {
this.title = title;
this.completed = completed;
}
// Getters and Setters
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;
}
}In this step, we’ve laid the groundwork for our ToDo list CRUD API with the Todo entity. We created the Todo class as a JPA entity, featuring attributes id, title, and completed. We designed constructors to initialize instances with or without values, and implemented getters and setters to ensure controlled attribute access. This entity is now ready for database persistence. In the next step, we will create our Repository class
Step 3: Creating the Repository
In this step, we’ll establish the repository that acts as a bridge between our application and the database. The repository provides methods to perform CRUD (Create, Read, Update, Delete) operations on our Todo entities.
Create the Repository Interface
Inside your project, in the same package as your Todo entity class, create a new Java interface named TodoRepository. This interface will extend JpaRepository<Todo, Long>, where Todo is the entity class and Long is the type of the entity's primary key.
package com.example.todolist;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TodoRepository extends JpaRepository<Todo, Long> {
}Examining our Interface
The JpaRepository is an interface provided by Spring Data JPA. It offers a collection of predefined methods for database operations which you will later see called in our Controller class that we will implement. By extending JpaRepository, our TodoRepository gains methods like save, findById, findAll, delete, and more, without requiring explicit implementation.
By creating the TodoRepository interface, you've laid the foundation for accessing and manipulating ToDo data within the database (in this case our in memory H2 Database). In the upcoming steps, we'll build upon this by creating the controller and implementing CRUD operations that utilize this repository to manage ToDo items.
Step 4: Creating the ApiResponse Class
Before proceeding to build the controller, let’s introduce the ApiResponse class. This class will standardize our API responses, providing consistent feedback to clients whether an operation succeeds or encounters an error.
Create the ApiResponse Class
Within your project, in the same package as your Todo entity and TodoRepository, create a new Java class named ApiResponse. This class will encapsulate the structure of our API responses.
package com.example.todolist;
public class ApiResponse {
private boolean success;
private String message;
private Object data;
public ApiResponse(boolean success, String message, Object data) {
this.success = success;
this.message = message;
this.data = data;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}Examining the ApiResponse Class
The ApiResponse class comprises the following attributes: success, message, and data.
successindicates whether an operation was successful or not.messageprovides an informative message about the outcome.dataholds the payload, such as retrieved entities or other relevant data.
Benefits of the ApiResponse Class
The ApiResponse class brings valuable benefits to our API structure:
- It establishes a standardized response format, ensuring clarity and consistency.
- Users receive clear and informative feedback, both for successful operations and errors.
- Effective error handling and data extraction are enabled on the client side.
By incorporating the ApiResponse class, we enhance the overall user experience and simplify the management of API responses throughout the application. This will be particularly valuable as we proceed to build and handle CRUD operations in our controller.
Step 5: Building the Controller
In this step, we will create the TodoController class, which serves as the entry point for handling API requests related to ToDo list items. Through this controller, we'll implement endpoints for various HTTP methods such as GET, POST, PUT, and DELETE. We'll also explore how we manage errors and provide standardized responses using the ApiResponse class.
package com.example.todolist;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.example.todolist.exception.ResourceNotFoundException;
@RestController
@RequestMapping("/api/todos")
public class TodoController {
private final TodoRepository todoRepository;
@Autowired
public TodoController(TodoRepository todoRepository) {
this.todoRepository = todoRepository;
}
@GetMapping
public ResponseEntity<ApiResponse> getAllTodos() {
try {
List<Todo> todos = todoRepository.findAll();
return ResponseEntity.ok(new ApiResponse(true, "Todos retrieved successfully", todos));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ApiResponse(false, "Error retrieving todos", null));
}
}
@GetMapping("/{id}")
public ResponseEntity<ApiResponse> getTodoById(@PathVariable Long id) {
try {
Todo todo = todoRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Todo not found with id: " + id));
return ResponseEntity.ok(new ApiResponse(true, "Todo retrieved successfully", todo));
} catch (ResourceNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ApiResponse(false, "Error retrieving todo with id: " + id, null));
}
}
@PostMapping
public ResponseEntity<ApiResponse> createTodo(@RequestBody Todo todo) {
try {
Todo createdTodo = todoRepository.save(todo);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new ApiResponse(true, "Todo created successfully", createdTodo));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ApiResponse(false, "Error creating todo", null));
}
}
@PutMapping("/{id}")
public ResponseEntity<ApiResponse> updateTodo(@PathVariable Long id, @RequestBody Todo todoDetails) {
try {
Todo todo = todoRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Todo not found with id: " + id));
todo.setTitle(todoDetails.getTitle());
todo.setCompleted(todoDetails.isCompleted());
Todo updatedTodo = todoRepository.save(todo);
return ResponseEntity.ok(new ApiResponse(true, "Todo updated successfully", updatedTodo));
} catch (ResourceNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ApiResponse(false, "Error updating todo with id: " + id, null));
}
}
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse> deleteTodo(@PathVariable Long id) {
try {
Todo todo = todoRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Todo not found with id: " + id));
todoRepository.delete(todo);
return ResponseEntity.ok(new ApiResponse(true, "Todo deleted successfully", null));
} catch (ResourceNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ApiResponse(false, "Error deleting todo with id: " + id, null));
}
}
}In this completed TodoController class, we've built an essential layer for managing our ToDo list items via API endpoints. We've addressed common HTTP methods, including GET, POST, PUT, and DELETE, and integrated the ApiResponse class to standardize our responses. Additionally, we've demonstrated error handling using the ResourceNotFoundException for instances where specific ToDo items aren't found. Our ResourceNotFoundException class is simply just a class that extends the RunTimeException class.
With the TodoController in place, we've effectively established a controlled and organized means of interacting with our API, enabling users to perform CRUD operations on the ToDo list items.
Section 5: Running and Testing your Application
Running the Application
Now that we have finished implementing our endpoints, it’s time to run and test our API’s. Depending on whether you run your project from your IDE or terminal is up to you. In the case of this example we will run our project from the IDE.
In our SpringToolSuite IDE, right click on your project under the local server and select (Re)start then verify that your project has successfully started in the console.

Testing our Application
Now that we have started our API server, the last thing we want to do is test each of our endpoints and verify that they are functioning. In this example we will be using Postman to test our endpoints.
Testing GET Endpoints
To test GET endpoints like fetching all todos or a specific todo by ID:
- Select the request type as “GET.”
- Enter the URL for the desired endpoint (e.g.,
http://localhost:8080/api/todosfor all todos andhttp://localhost:8080/api/1(where 1 is an id) to get a todo by ID. - Click the “Send” button to receive the response Below is a sample response for retrieving all todos.
{
"success": true,
"message": "Todos retrieved successfully",
"data": [
{
"id": 1,
"title": "Get gas",
"completed": false
},
{
"id": 2,
"title": "Get Groceries",
"completed": false
}
]
}Testing POST, PUT, and DELETE Endpoints
For POST, PUT, and DELETE requests, follow similar steps:
- Select the respective request type.
- Set the URL for the desired endpoint (e.g.,
http://localhost:8080/api/todosfor creating a todo). - In the “Body” section, provide the necessary data in JSON format (for POST and PUT requests). Sample Request Body for creating a todo:
{
"title": "Get Groceries",
"completed": false
}- Click the “Send” button to observe the response. Sample Response body after creating todo:
{
"success": true,
"message": "Todo created successfully",
"data": {
"id": 2,
"title": "Get Groceries",
"completed": false
}
}Conclusion
This concludes our tutorial on developing and testing a CRUD REST API Project using the Java Spring Boot Framework.
In summary, you’ve learned to:
- Initialize a Spring Boot project with essential dependencies using the Spring Initializr.
- Design a data model by creating the
Todoentity with attributes and methods. - Create a repository for seamless interaction with the database.
- Build a controller to handle API requests and perform CRUD operations.
- Test your API endpoints using Postman to ensure functionality and reliability.
Feel free to share any feedback on how this example can be improved and I hope this article has been helpful. Original documentation: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/
Source Code: https://github.com/mwiginton/springboot-rest-crud-example
If you enjoyed reading this article, please consider signing up for Medium if you haven’t already done so using my referral link. This subscription ensures unlimited access to my articles plus thousands of other talented writers across many disciplines.






