Build a Task Manager REST API
Build a full CRUD REST API with Spring Boot, JPA, and H2 database.
Introduction
30 Second Summary
Every app you use daily has a backend. When you tap "add task" in a to-do app, a server somewhere catches that request, saves your task, and sends back a confirmation. That invisible layer is a REST API.
In this project, you will build a Task Manager REST API using Java and Spring Boot. You will create, read, update, and delete tasks by sending HTTP requests to a server running on your own machine.
What You'll Build
You will open Postman, send a POST request with a task title and description, and watch your API respond with the saved task including its auto-generated ID.
By the end of this project, you'll have:
- A running REST API that creates, lists, updates, and deletes tasks through HTTP requests you fire from Postman.
- Graceful error handling that responds with clean JSON messages when something goes wrong, instead of crashing with a generic server error.
- A Spring Boot project structured in the professional Controller/Service/Repository pattern that separates your code into clean, maintainable layers.
- Secret Mission: Add query parameter filtering so you can fetch only completed or incomplete tasks with a single URL change.
Do I need Java experience?
No prior Java or API experience is required. You just need basic programming experience with variables, loops, functions, and classes in any language.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Java Development Environment
You cannot write or run any Java code without a JDK installed on your machine.
The JDK (Java Development Kit) includes the compiler and runtime that turn your source code into a running application. In this step, you will install JDK 21, configure VS Code with Java extensions, and set up Postman for testing your API.
In this step, get ready to:
- Install JDK 21 and verify it works from the command line.
- Add Java and Spring Boot extensions to VS Code.
- Install Postman for sending HTTP requests to your API.
Install JDK 21 on Windows
The JDK is what lets your computer compile and run Java code. You need version 21 specifically because it is the current Long-Term Support (LTS) release that most production teams and tutorials target.
- Open your browser and navigate to the Oracle JDK downloads page:
- https://www.oracle.com/java/technologies/downloads/
- Scroll down to the JDK 21 section.
- Click the Windows tab.
- Download the x64 Installer (the .exe file named jdk-21_windows-x64_bin.exe).
- Double-click the downloaded jdk-21_windows-x64_bin.exe file to start the installer.
- Follow the prompts and accept the defaults. The installer requires administrator privileges.
What does the installer do?
The installer places Java executables (java.exe, javac.exe) into a shared path on your system so you can run them from any Command Prompt window. You do not need to configure PATH manually.
Now verify the installation worked. You must open a brand new Command Prompt window because the old one does not know about the new PATH entries.
- Press the Windows key to open your search bar.
- Type cmd and press Enter to open Command Prompt.
- Check the Java runtime version by running this command:
java -version
What do you expect to see? Take a moment to predict the output before reading on.
✔️ I see a version starting with 21
You should see output that begins with a version number starting with 21. That confirms the JDK is installed and on your PATH.
- Also confirm the compiler is available by running:
javac -version
You should see a matching version number starting with 21. You are all set.
ⓧ I see an older version number
If you see a version lower than 21 (for example 1.8.0 or 17.0.x), an older Java installation is taking precedence on your PATH.
- Download and run the jdk-21_windows-x64_bin.exe installer from the Oracle downloads page linked above. The JDK 21 installer updates the shared javapath entry so version 21 takes priority.
- If a legacy JRE 8 is installed, it may have placed its own PATH entry in front of JDK 21. Uninstall JRE 8 via Settings > Apps > Installed apps, then reinstall JDK 21 to refresh the PATH.
- Close your current Command Prompt window and open a new one.
- Run java -version again and confirm it now shows 21.
ⓧ Command not found
If you see an error like "'java' is not recognized as an internal or external command", the JDK is not installed or the PATH was not updated.
- Make sure you downloaded the .exe installer (not the .zip) from the Oracle downloads page linked above. The .exe installer sets up PATH automatically.
- Run the installer again and accept all defaults.
- Close your current Command Prompt window and open a brand new one. PATH changes only take effect in new windows.
- Run java -version again.
If it still does not work, you may need to add the JDK bin directory to your system PATH manually. The default install location is C:\Program Files\Java\jdk-21\bin.
Install VS Code extensions
VS Code does not understand Java out of the box. Two extension packs add full language support, debugging, and Spring Boot tooling.
- Switch back to VS Code.
- Open the Extensions view by pressing Ctrl+Shift+X.
- In the search bar, type Extension Pack for Java.
- Find the result published by Microsoft and click Install.
What does Extension Pack for Java include?
This single install gives you Java language support (syntax highlighting, IntelliSense), a debugger, test runner, Maven integration, and project management. It bundles six extensions into one click.
- Clear the search bar and type Spring Boot Extension Pack.
- Find the result published by VMware and click Install.
What does Spring Boot Extension Pack include?
This pack adds Spring Boot Tools (smart navigation, live app info), the Spring Initializr extension (project generator), and the Spring Boot Dashboard. You will use Spring Initializr in the next step to scaffold your project.
Confirm both packs appear in your Installed list in the Extensions sidebar. You should see them listed with a green checkmark.
Extensions not appearing?
Make sure you searched for the exact names above. If VS Code prompts you to reload, click Reload to activate the extensions.
Still stuck? Help me troubleshoot my VS Code Java extensions.
Install Postman
Postman is the tool you will use to send HTTP requests to your API and inspect the responses. Think of it as a control panel for talking to your backend.
- Open your browser and go to the Postman downloads page:
- https://www.postman.com/downloads/
- Click the Windows 64-bit download button.
- Run the downloaded installer. It installs and opens automatically.
- When Postman opens, you can create a free account or click Skip and go to the app to use the lightweight client.
You should now see the Postman main workspace with an option to create a new request. That confirms Postman is ready.
Postman not opening?
Make sure the download completed fully before running the installer. If Windows SmartScreen blocks the app, click More info and then Run anyway.
Need more help? Help me get Postman working on Windows.
Your development environment is locked and loaded. Next up, you will use these tools to create a Spring Boot project and see it respond to its very first HTTP request.
Create Your Spring Boot Project and First Endpoint
You have Java and your editor ready. Now you need an actual application that starts a web server and responds when someone visits a URL.
In this step, you will generate a Spring Boot project, write your first REST endpoint, and see it respond in both a browser and Postman.
In this step, get ready to:
- Generate a Spring Boot project using Spring Initializr inside VS Code.
- Write a controller that responds to GET requests with "Hello, World!".
- Test the endpoint in your browser and in Postman.
Generate the project with Spring Initializr
Spring Initializr is a tool that generates a ready-to-run Spring Boot project with the dependencies you choose. The VS Code extension connects to it directly so you never have to leave your editor.
- Open the Command Palette by pressing Ctrl+Shift+P.
- Type Spring Initializr: Create a Maven Project and select it from the list.
What happens next?
The wizard asks you a series of questions one at a time. Each prompt appears at the top of VS Code. You select or type a value, then press Enter to move to the next prompt.
- Select Spring Boot version 4.1.0.
- Select Java as the project language.
- Enter com.example for the Group Id.
- Enter taskmanager for the Artifact Id.
- Select Jar for the packaging type.
- Select Java version 21.
- Search for and add these four dependencies: Spring Web, Spring Data JPA, H2 Database, and Validation. After selecting all four, press Enter to proceed.
- Choose your Desktop as the folder to save the project, then click Generate into this folder.
VS Code asks if you want to open the generated project. Click Open.
What is Spring Initializr doing?
It downloaded a pre-configured Maven project from start.spring.io. The project includes a pom.xml file (which lists your dependencies), a main application class, and the Maven Wrapper so you can build the project without installing Maven separately.
- Confirm the project opened by checking the Explorer sidebar on the left. You should see folders like src, .mvn, and a pom.xml file at the top level.
Don't see the project structure?
Make sure you clicked Open when VS Code prompted you. If you missed it, click File then Open Folder and navigate to the taskmanager folder on your Desktop.
If the Command Palette did not show the Spring Initializr command, confirm the Spring Boot Extension Pack is installed in VS Code.
Help me troubleshoot this issue.
Write your first REST controller
Your project has a main application class, but it has no endpoints yet. A controller is a Java class that tells Spring Boot which URLs your app responds to and what it sends back.
- In the Explorer sidebar, expand src then main then java then com then example then taskmanager.
- Right-click the taskmanager folder and select New File.
- Name the file HelloController.java and press Enter.
- Paste the following code into the new file:
package com.example.taskmanager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
// Marks this class as a REST controller that returns data directly
@RestController
public class HelloController {
// Responds to GET requests at /hello
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}
What does this code do?
`@RestController` tells Spring Boot this class handles incoming web requests and returns the response body directly (not a webpage template).
`@GetMapping("/hello")` maps HTTP GET requests at the /hello URL path to the hello() method. When someone visits that URL, Spring Boot calls this method and sends back the returned string.
- Save the file with Ctrl+S.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, usually above Tab).
- Start your application by running this command:
.\mvnw.cmd spring-boot:run
What does this command do?
`mvnw.cmd` is the Maven Wrapper that ships with your project. It downloads the correct Maven version automatically so you do not need to install Maven yourself.
`spring-boot:run` tells Maven to compile your code and start the embedded Tomcat web server on port 8080.
Wait for the terminal to show a line containing Started TaskmanagerApplication. This confirms the server is running.
Before you open the browser, what do you think you will see at http://localhost:8080/hello?
- Open your browser and navigate to http://localhost:8080/hello.
You should see the plain text Hello, World! displayed in the browser. That is your first live API endpoint.
Seeing a Whitelabel Error Page or connection refused?
If you see connection refused, the server is not running. Check the terminal for error messages. A common issue is another process already using port 8080.
If you see a Whitelabel Error Page, double-check the URL path is exactly /hello (not /Hello or /). URL paths are case-sensitive.
If you see compilation errors in the terminal, open HelloController.java and confirm the package line reads package com.example.taskmanager; exactly.
Help me troubleshoot my Spring Boot application.
✔️ Awesome, I've got everything!
Great. Make sure you have saved HelloController.java and the server is still running in your terminal.
ⓧ I'd like to double check the full code
Here is the complete HelloController.java file. Compare it to yours character by character:
package com.example.taskmanager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}
Make your first Postman request
Browsers can only send GET requests easily. Postman lets you send any type of HTTP request (GET, POST, PUT, DELETE) and inspect the full response. You will use it heavily throughout this project.
- Switch to Postman (it should still be open from Step 1).
- Click the + button to create a new request tab.
- Make sure the method dropdown says GET.
- Enter http://localhost:8080/hello in the URL field.
- Click Send.
In the response panel at the bottom, you should see a status of 200 OK and the body text Hello, World!.
What just happened?
You sent an HTTP GET request to your Spring Boot server. The server matched the URL to your @GetMapping("/hello") method, ran it, and returned the string as the response body.
The 200 OK status means the request succeeded. Every backend interaction you build from here on works this same way.
Seeing "Could not send request" in Postman?
This means Postman cannot reach the server. Confirm your Spring Boot app is still running in the VS Code terminal. If you accidentally closed the terminal or pressed Ctrl+C, the server stopped. Re-run .\mvnw.cmd spring-boot:run to start it again.
Help me fix this Postman connection issue.
Your server is alive and answering requests. Next up, you will give it something real to manage by creating a Task entity and connecting it to a database.
Build the Task Entity and Repository
Your API can say hello, but it has no memory. It cannot store tasks, retrieve them, or even remember they exist after you restart the server.
In this step, you will create a Task entity that maps to a database table and connect it to an H2 in-memory database. By the end, you will be able to see your TASKS table in a browser-based database console.
In this step, get ready to:
- Create a Task entity class with JPA annotations that maps to a database table.
- Create a TaskRepository interface that gives you full CRUD operations with zero SQL.
- Configure the H2 database and verify the TASKS table exists through the H2 console.
Create the Task entity
A JPA entity is a Java class that maps directly to a database table. Each instance of the class becomes a row in the table, and each field becomes a column.
- In VS Code's Explorer sidebar, right-click on the src/main/java/com/example/taskmanager/ folder.
- Select New Folder and name it model.
- Right-click the new model folder, select New File, and name it Task.java.
- Paste the following code into Task.java to define the entity class with its fields:
package com.example.taskmanager.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
// Maps this class to a database table called "tasks"
@Entity
@Table(name = "tasks")
public class Task {
// Primary key, auto-incremented by the database
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String description;
private boolean completed = false;
}
What does this code do?
The @Entity annotation tells JPA this class represents a database table. @Table(name = "tasks") specifies the table name.
The @Id annotation marks the primary key. @GeneratedValue(strategy = GenerationType.IDENTITY) tells the database to auto-generate unique IDs for each new task.
The completed field defaults to false so every new task starts as incomplete.
- Now add the constructors and getter/setter methods below the completed field, inside the class (before the closing }):
public Task() {
}
public Task(String title, String description) {
this.title = title;
this.description = description;
}
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 String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public boolean isCompleted() { return completed; }
public void setCompleted(boolean completed) { this.completed = completed; }
Why are getters and setters needed?
JPA requires a no-argument constructor (public Task()) to create entity instances from database rows. The getters and setters let JPA read and write each field.
The second constructor is a convenience for your own code. It lets you create a new Task with a title and description in one line.
- Save Task.java.
- Confirm in the Explorer sidebar that you see model folder containing Task.java under src/main/java/com/example/taskmanager/.
Seeing red underlines in Task.java?
If VS Code highlights jakarta.persistence imports as errors, the Java extension may still be loading your project's dependencies. Wait a few seconds for the language server to finish indexing.
If errors persist, try opening the Command Palette (Ctrl+Shift+P) and running Java: Clean Java Language Server Workspace.
Help me fix red underlines in my Task.java entity file.
✔️ Awesome, I've got everything!
Great. Double check you have saved Task.java and that the file has no red error indicators.
ⓧ I'd like to double check the full code
Your Task.java should have two sections. Scroll up and confirm:
- The first section contains the package declaration, five jakarta.persistence imports, the @Entity and @Table annotations, and four private fields (id, title, description, completed).
- The second section contains two constructors (no-arg and parameterized) followed by getters and setters for all four fields.
Create the TaskRepository interface
Spring Data JPA can generate a full set of database operations from a single interface. You do not need to write any SQL or implement the methods yourself.
- Right-click on the src/main/java/com/example/taskmanager/ folder in the Explorer sidebar.
- Select New Folder and name it repository.
- Right-click the new repository folder, select New File, and name it TaskRepository.java.
- Paste the following code into TaskRepository.java:
package com.example.taskmanager.repository;
import com.example.taskmanager.model.Task;
import org.springframework.data.jpa.repository.JpaRepository;
// Extends JpaRepository to get save, findAll, findById, deleteById for free
public interface TaskRepository extends JpaRepository<Task, Long> {
}
What does this code do?
By extending JpaRepository<Task, Long>, Spring automatically provides methods like save(), findAll(), findById(), and deleteById().
The two type parameters tell Spring that this repository manages Task entities and that the primary key type is Long. That single line gives you full CRUD operations with zero SQL.
- Save TaskRepository.java.
- Confirm in the Explorer sidebar that you now see both a model folder and a repository folder under taskmanager.
Import errors on JpaRepository?
If org.springframework.data.jpa.repository.JpaRepository shows a red underline, your project's Maven dependencies may not have finished downloading. Open the terminal and run:
.\mvnw.cmd dependency:resolve
Wait for the download to complete, then reopen the file.
Help me resolve JpaRepository import errors in VS Code.
Configure H2 and verify the database
Your entity and repository are ready, but Spring Boot needs to know which database to connect to and that you want the H2 browser console available for inspection.
- Open src/main/resources/application.properties in VS Code.
- Replace the file's entire contents with the following configuration:
spring.application.name=taskmanager
# Connect to an in-memory H2 database named "taskdb"
spring.datasource.url=jdbc:h2:mem:taskdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# Tell Hibernate to auto-create/update tables from entities
spring.jpa.hibernate.ddl-auto=update
# Print SQL statements to the console for debugging
spring.jpa.show-sql=true
# Enable the H2 browser console at /h2-console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
What do these properties do?
The spring.datasource.url points to an in-memory database called taskdb. It lives only while the application is running and resets on each restart.
Setting spring.jpa.hibernate.ddl-auto=update tells Hibernate to automatically create or modify database tables to match your entity classes. The show-sql property prints every SQL statement to the terminal so you can see what happens behind the scenes.
The H2 console gives you a browser-based interface to inspect your database, similar to a lightweight version of tools like pgAdmin or MySQL Workbench.
- Save application.properties.
- If your application is still running from the previous step, stop it by pressing Ctrl+C in the terminal.
- Restart the application by running this command:
.\mvnw.cmd spring-boot:run
Watch the terminal output. You should see a line containing create table tasks in the SQL output, which confirms Hibernate created your TASKS table automatically.
Before you open the H2 console, what do you think you will find inside the TASKS table?
- Open your browser and navigate to http://localhost:8080/h2-console.
- In the JDBC URL field, enter jdbc:h2:mem:taskdb.
- Leave User Name as sa and Password empty.
- Click Connect.
You should see a TASKS table listed in the left sidebar. It will be empty for now, which is exactly right. You have not created any tasks yet.
Don't see the TASKS table?
Make sure the JDBC URL field says jdbc:h2:mem:taskdb exactly. The default value is often jdbc:h2:mem:testdb which connects to a different (empty) database.
If you see no tables at all, check that your application.properties has spring.datasource.url=jdbc:h2:mem:taskdb and restart the application.
Help me connect to my H2 console and see the TASKS table.
Your database layer is in place. You have a Task entity that maps to a table and a repository that provides database operations. Next up, you will build the service and controller layers that expose these operations as HTTP endpoints.
Create the Service Layer and REST Endpoints
You have a Task entity and a repository, but no way to interact with them from the outside world. Your API can only say "Hello, World!" right now.
In this step, you will create a service layer for business logic and a controller that exposes HTTP endpoints. By the end, you will be able to create, read, update, and delete tasks through Postman.
In this step, get ready to:
- Create the TaskService class with CRUD business logic.
- Build a TaskController with GET, POST, PUT, and DELETE endpoints.
- Test the full CRUD cycle in Postman.
Create the TaskService class
The service layer sits between your controller and your repository. It holds business logic and keeps your controller thin.
What is a service layer?
In the Controller/Service/Repository pattern, the controller handles HTTP concerns (request/response), the service handles business rules, and the repository talks to the database.
Right now the logic is simple (just forwarding calls to the repository), but as your app grows you would add validation, transformation, and orchestration here without touching the controller.
- In the VS Code Explorer sidebar, right-click the src/main/java/com/example/taskmanager/ folder and select New Folder.
- Name the folder service.
- Right-click the new service folder and select New File.
- Name the file TaskService.java and paste the following code:
package com.example.taskmanager.service;
import com.example.taskmanager.model.Task;
import com.example.taskmanager.repository.TaskRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class TaskService {
private final TaskRepository taskRepository;
// Spring automatically injects the TaskRepository here
public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
// Return all tasks from the database
public List<Task> getAllTasks(Boolean completed) {
return taskRepository.findAll();
}
// Find a single task by its ID
public Task getTaskById(Long id) {
return taskRepository.findById(id).orElse(null);
}
// Save a new task to the database
public Task createTask(Task task) {
return taskRepository.save(task);
}
}
What does this code do?
- @Service tells Spring this class contains business logic and should be managed as a bean.
- The constructor accepts a TaskRepository parameter. Spring automatically provides the repository instance. This is called dependency injection.
- findById(id).orElse(null) returns the task if found, or null if the ID does not exist in the database.
Now add the update and delete methods to complete the service.
- In TaskService.java, find the closing brace } at the bottom of the class.
- Add the following two methods just above that closing brace:
// Update an existing task's fields
public Task updateTask(Long id, Task taskDetails) {
Task task = getTaskById(id);
task.setTitle(taskDetails.getTitle());
task.setDescription(taskDetails.getDescription());
task.setCompleted(taskDetails.isCompleted());
return taskRepository.save(task);
}
// Remove a task from the database
public void deleteTask(Long id) {
taskRepository.deleteById(id);
}
What does this code do?
- updateTask fetches the existing task, overwrites its title, description, and completed status with the new values, then saves it back to the database.
- deleteTask removes the task by its ID using the repository's built-in deleteById method.
- Save TaskService.java.
Seeing red underlines in VS Code?
If your editor shows errors on the import lines, make sure the service folder is inside src/main/java/com/example/taskmanager/ and not at the project root.
Also confirm the package statement at the top reads com.example.taskmanager.service exactly.
help me fix red underlines in my TaskService.java file
✔️ Awesome, I've got everything!
Great. Make sure you have saved TaskService.java before moving on.
ⓧ I'd like to double check the full code
Your TaskService.java should contain these sections in order:
- Package declaration and imports (Task, TaskRepository, Service, List).
- Class declaration with @Service and constructor injecting TaskRepository.
- getAllTasks method returning taskRepository.findAll().
- getTaskById method using findById(id).orElse(null).
- createTask method calling taskRepository.save(task).
- updateTask method that fetches, updates fields, and saves.
- deleteTask method calling taskRepository.deleteById(id).
Create the TaskController with GET and POST endpoints
The controller is the front door of your API. It maps HTTP requests to your service methods and sends back responses.
- In the Explorer sidebar, right-click the src/main/java/com/example/taskmanager/ folder and select New Folder.
- Name the folder controller.
- Right-click the new controller folder and select New File.
- Name the file TaskController.java and paste the following code:
package com.example.taskmanager.controller;
import com.example.taskmanager.model.Task;
import com.example.taskmanager.service.TaskService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService taskService;
// Spring injects the TaskService automatically
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
// GET /api/tasks - return all tasks
@GetMapping
public List<Task> getAllTasks(@RequestParam(required = false) Boolean completed) {
return taskService.getAllTasks(completed);
}
}
What does this code do?
- @RestController tells Spring this class handles HTTP requests and returns data directly (not a web page).
- @RequestMapping("/api/tasks") sets the base URL path. All endpoints in this controller start with /api/tasks.
- @GetMapping maps HTTP GET requests to the getAllTasks method. When someone visits /api/tasks, this method runs.
Now add the endpoint for fetching a single task and the endpoint for creating a task.
- In TaskController.java, find the closing brace } at the bottom of the class.
- Add the following two methods just above that closing brace:
// GET /api/tasks/{id} - return one task by ID
@GetMapping("/{id}")
public Task getTaskById(@PathVariable Long id) {
return taskService.getTaskById(id);
}
// POST /api/tasks - create a new task
@PostMapping
public Task createTask(@RequestBody Task task) {
return taskService.createTask(task);
}
What does this code do?
- @GetMapping("/{id}") captures the number from the URL (like /api/tasks/3) and passes it as the id parameter.
- @RequestBody tells Spring to read the JSON from the request body and convert it into a Task object automatically.
- Save TaskController.java.
- If your Spring Boot application is still running from earlier, press Ctrl+C in the terminal to stop it.
- Restart the application by running:
.\mvnw.cmd spring-boot:run
Before you test, what do you think will happen if you send a POST request with a task title and description?
- Switch to Postman.
- Click the + button to create a new request tab.
- Change the method dropdown from GET to POST.
- Enter http://localhost:8080/api/tasks in the URL field.
- Click the Body tab below the URL field.
- Select raw and change the dropdown on the right from Text to JSON.
- Paste this JSON into the body:
{
"title": "Learn Spring Boot",
"description": "Build a REST API from scratch"
}
- Click Send.
You should see a 200 OK status and a JSON response containing your task with an auto-generated id field.
- Now create a new GET request to http://localhost:8080/api/tasks and click Send.
You should see an array containing the task you just created.
Getting a 404 or connection error?
- Make sure the Spring Boot application is running. Check the terminal for the "Started TaskmanagerApplication" message.
- Verify the URL is exactly http://localhost:8080/api/tasks with no trailing slash.
- If you see a 415 error, make sure you selected JSON (not Text) in the Body format dropdown.
help me troubleshoot my Spring Boot API not responding to requests in Postman
Add PUT and DELETE endpoints
Your API can create and read tasks. Now you need update and delete to complete the full CRUD cycle.
- In TaskController.java, add this import at the top of the file, below the existing import lines:
import org.springframework.http.ResponseEntity;
- Find the closing brace } at the bottom of the class and add these two methods just above it:
// PUT /api/tasks/{id} - update an existing task
@PutMapping("/{id}")
public Task updateTask(@PathVariable Long id, @RequestBody Task taskDetails) {
return taskService.updateTask(id, taskDetails);
}
// DELETE /api/tasks/{id} - remove a task
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
taskService.deleteTask(id);
return ResponseEntity.noContent().build();
}
What does this code do?
- @PutMapping("/{id}") handles HTTP PUT requests. The request body provides the new values to overwrite.
- @DeleteMapping("/{id}") handles HTTP DELETE requests. It returns a 204 No Content response (the standard way to confirm a delete with no body to send back).
- ResponseEntity<Void> gives you control over the HTTP status code. ResponseEntity.noContent().build() creates a 204 response.
- Save TaskController.java.
- Stop the running application with Ctrl+C in the terminal and restart it by running:
.\mvnw.cmd spring-boot:run
Time to test the full CRUD cycle. You will create a task, update it, and delete it.
- In Postman, send a POST to http://localhost:8080/api/tasks with this JSON body:
{
"title": "Buy groceries",
"description": "Milk, eggs, bread"
}
- Note the id in the response (it should be 1 if this is your first task after restart).
- Create a new PUT request to http://localhost:8080/api/tasks/1 with this JSON body:
{
"title": "Buy groceries",
"description": "Milk, eggs, bread, butter",
"completed": true
}
- Click Send. You should see the updated task with completed set to true.
- Create a new DELETE request to http://localhost:8080/api/tasks/1 and click Send.
You should see a 204 No Content response, confirming the task is deleted.
- Send a GET request to http://localhost:8080/api/tasks to confirm the list is now empty.
Now try something that should break. What do you think will happen if you request a task that does not exist?
- Send a GET request to http://localhost:8080/api/tasks/999 and click Send.
You will see a 500 Internal Server Error. The response body contains a generic error with no helpful message for the caller.
Why does this happen?
Your getTaskById method returns null when the task is not found. When Spring tries to serialize null or the controller tries to use it, things break in an ugly way.
Real APIs should never expose raw 500 errors to their users. They should return meaningful status codes (like 404) with helpful messages. That is exactly what the next step fixes.
✔️ Awesome, I've got everything!
Great. Make sure both TaskService.java and TaskController.java are saved.
ⓧ I'd like to double check the full code
Your TaskController.java should contain these sections in order:
- Package declaration and imports (Task, TaskService, ResponseEntity, web.bind.annotation.*, List).
- Class declaration with @RestController and @RequestMapping("/api/tasks").
- Constructor injecting TaskService.
- getAllTasks with @GetMapping and optional completed parameter.
- getTaskById with @GetMapping("/{id}").
- createTask with @PostMapping.
- updateTask with @PutMapping("/{id}").
- deleteTask with @DeleteMapping("/{id}") returning ResponseEntity<Void>.
Your API handles the happy path perfectly. You can create, read, update, and delete tasks. But it falls apart when something goes wrong. Next up, you will add proper error handling so your API returns clean, helpful error messages instead of crashing.
Add Error Handling and Input Validation
In the previous step, you hit a wall: requesting a task that does not exist returned an ugly 500 Internal Server Error. That raw stack trace is what your users would see in production.
Your API also happily accepts tasks with completely blank titles. In this step, you will add proper error handling that returns clean JSON error messages, and input validation that rejects bad data before it reaches the database.
In this step, get ready to:
- Create a custom exception class and a global error handler that returns JSON responses.
- Add validation annotations to reject tasks with blank titles or oversized descriptions.
- Test all error scenarios in Postman to confirm your API fails gracefully.
Create a custom exception and global handler
Right now, when TaskService cannot find a task, it returns null. The controller then tries to use that null value, which causes the 500 error. You need a dedicated exception that the service throws, and a handler that catches it and returns a proper 404 response.
- In the VS Code Explorer sidebar, right-click the src/main/java/com/example/taskmanager/ folder and select New Folder.
- Name the folder exception.
- Inside the new exception folder, create a file called ResourceNotFoundException.java and paste the following code:
package com.example.taskmanager.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
What does this code do?
This is a custom exception class that extends RuntimeException. When the service cannot find a task, it will throw this exception with a descriptive message like "Task not found with id: 5".
Spring Boot treats unchecked exceptions (ones extending RuntimeException) specially. You can intercept them with a global handler and transform them into clean HTTP responses.
- Save ResourceNotFoundException.java.
- Confirm the file appears under exception/ in the Explorer sidebar.
- In the same exception folder, create a file called GlobalExceptionHandler.java and paste the following code:
package com.example.taskmanager.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
// Catch ResourceNotFoundException and return a 404 JSON response
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Map<String, String>> handleResourceNotFound(ResourceNotFoundException ex) {
Map<String, String> error = new HashMap<>();
error.put("error", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
What does this code do?
The @RestControllerAdvice annotation tells Spring Boot this class handles exceptions across all controllers. You do not need to add error handling code in each controller individually.
The @ExceptionHandler method catches any ResourceNotFoundException thrown anywhere in the app and returns a 404 status with a JSON body like {"error": "Task not found with id: 5"}.
- Save GlobalExceptionHandler.java.
- Confirm both files appear under the exception package in the Explorer sidebar.
Don't see the exception package?
Make sure you created the exception folder inside src/main/java/com/example/taskmanager/ and not at the project root. Java packages must live in the correct directory structure to be detected by Spring Boot.
help me find where to put my exception package
Now you need to update TaskService.java to actually throw this exception instead of returning null.
- Open src/main/java/com/example/taskmanager/service/TaskService.java.
- Find the import block at the top and add this new import:
import com.example.taskmanager.exception.ResourceNotFoundException;
- Find the getTaskById method. It currently looks like this:
public Task getTaskById(Long id) {
return taskRepository.findById(id).orElse(null);
}
- Replace the entire method body so it throws the exception when the task is not found:
public Task getTaskById(Long id) {
return taskRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Task not found with id: " + id));
}
- Find the deleteTask method. It currently looks like this:
public void deleteTask(Long id) {
taskRepository.deleteById(id);
}
- Replace it with this version that uses getTaskById to verify the task exists before deleting:
public void deleteTask(Long id) {
Task task = getTaskById(id);
taskRepository.delete(task);
}
Why update deleteTask too?
By calling getTaskById(id) first, the delete method now also throws a 404 if the task does not exist. Without this change, deleting a non-existent task would silently succeed or throw a generic error.
- Save TaskService.java.
- Stop the running application by pressing Ctrl+C in the terminal.
- Restart the application by running:
.\mvnw.cmd spring-boot:run
Before you test, think about what you expect to see when you request a task that does not exist. Will it still be a 500 error, or something different?
- Switch to Postman and send a GET request to http://localhost:8080/api/tasks/999.
- Click Send.
You should now see a 404 Not Found status with a clean JSON response body:
{
"error": "Task not found with id: 999"
}
That is a massive improvement over the raw 500 stack trace from before.
Still seeing a 500 error?
Make sure you added the import com.example.taskmanager.exception.ResourceNotFoundException; line at the top of TaskService.java. Without it, the file will not compile.
Also check that GlobalExceptionHandler.java is inside the exception package, not in a different folder. Spring Boot needs to scan it as part of the application.
help me debug my 500 error when fetching a non-existent task
Add validation annotations to the Task entity
Your API now handles missing tasks gracefully. But it still accepts tasks with completely blank titles, which is bad data that should never reach the database.
- Open src/main/java/com/example/taskmanager/model/Task.java.
- Find the import block at the top of the file (the lines starting with import jakarta.persistence) and add these two new imports below them:
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
- Find the title field declaration. It currently looks like this:
private String title;
- Add the @NotBlank annotation above it so it looks like this:
@NotBlank(message = "Title is required")
private String title;
- Find the description field declaration directly below. It currently looks like this:
private String description;
- Add the @Size annotation above it so it looks like this:
@Size(max = 500, message = "Description must be less than 500 characters")
private String description;
- Save Task.java.
What do these annotations do?
@NotBlank rejects null values, empty strings, and strings that are only whitespace. @Size(max = 500) sets an upper limit on the description length.
The message attribute on each annotation controls the exact error text returned to the API consumer. These annotations define the rules, but the controller must be told to enforce them.
Next, you need to tell the controller to check these validation rules before processing a request.
- Open src/main/java/com/example/taskmanager/controller/TaskController.java.
- Find the import block at the top and add this import:
import jakarta.validation.Valid;
- Find the createTask method. It currently looks like this:
@PostMapping
public Task createTask(@RequestBody Task task) {
- Add @Valid before @RequestBody so the line looks like this:
@PostMapping
public Task createTask(@Valid @RequestBody Task task) {
- Find the updateTask method. It currently looks like this:
@PutMapping("/{id}")
public Task updateTask(@PathVariable Long id, @RequestBody Task taskDetails) {
- Add @Valid before @RequestBody here too:
@PutMapping("/{id}")
public Task updateTask(@PathVariable Long id, @Valid @RequestBody Task taskDetails) {
- Save TaskController.java.
Finally, you need a handler that catches validation errors and returns a clean 400 response with the field-level details.
- Open src/main/java/com/example/taskmanager/exception/GlobalExceptionHandler.java.
- Add this import at the top, below the existing imports:
import org.springframework.web.bind.MethodArgumentNotValidException;
- Add this new handler method inside the class, below the handleResourceNotFound method:
// Catch validation errors and return a 400 with field-level details
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(fieldError ->
errors.put(fieldError.getField(), fieldError.getDefaultMessage())
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
What does this handler do?
When @Valid detects that incoming data violates your annotations, Spring throws a MethodArgumentNotValidException. This handler catches it and loops through all the field errors.
It builds a map where each key is the field name (like title) and each value is the error message (like "Title is required"). This gives API consumers a clear, structured response they can parse.
- Save GlobalExceptionHandler.java.
- Stop the running application with Ctrl+C and restart it:
.\mvnw.cmd spring-boot:run
- In Postman, create a new POST request to http://localhost:8080/api/tasks.
- Click the Body tab, select raw, and set the type to JSON.
- Paste this JSON body with an empty title:
{
"title": "",
"description": "This task has no title"
}
- Click Send.
You should see a 400 Bad Request status with this JSON body:
{
"title": "Title is required"
}
Your API now rejects bad data with a helpful error message instead of silently saving it.
Still getting 200 OK with an empty title?
Make sure you added @Valid before @RequestBody in the createTask method of TaskController.java. Without @Valid, the annotations on Task.java are ignored.
Also confirm you imported jakarta.validation.Valid (not javax.validation.Valid). Spring Boot 4.1.0 uses the Jakarta namespace.
help me fix my validation not working in Spring Boot
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved: Task.java, TaskController.java, and GlobalExceptionHandler.java.
ⓧ I'd like to double check the full code
Here is the complete GlobalExceptionHandler.java with both handlers:
package com.example.taskmanager.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Map<String, String>> handleResourceNotFound(ResourceNotFoundException ex) {
Map<String, String> error = new HashMap<>();
error.put("error", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(fieldError ->
errors.put(fieldError.getField(), fieldError.getDefaultMessage())
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}
For TaskController.java and Task.java, scroll up and confirm each annotation change is in place. The key edits: @NotBlank on title, @Size on description, @Valid on both createTask and updateTask parameters.
Test error scenarios in Postman
Your error handling and validation are wired up. Now run through every scenario to confirm the API responds correctly in all cases.
- In Postman, send a GET request to http://localhost:8080/api/tasks/999.
- Confirm you see 404 Not Found with {"error": "Task not found with id: 999"}.
- Create a new POST request to http://localhost:8080/api/tasks with this valid JSON body:
{
"title": "Learn Spring Boot",
"description": "Complete the task manager project"
}
- Click Send and confirm you see a 200 OK with the created task and an auto-generated id.
Why test the happy path?
When you add error handling, it is easy to accidentally break the normal flow. Always confirm that valid requests still succeed after adding validation rules.
- Send a DELETE request to http://localhost:8080/api/tasks/1 (or whatever ID was returned from the POST above).
- Confirm you get a 204 No Content response.
Now, what do you think will happen if you try to GET that same task you just deleted?
- Send a GET request to the same task URL (e.g. http://localhost:8080/api/tasks/1).
- Confirm you see 404 Not Found with the error message confirming the task no longer exists.
Every error scenario now returns a clean, structured JSON response that any API consumer can understand and handle.
Your API is now production-ready in its error handling. It responds with appropriate HTTP status codes and clear error messages for every failure scenario. Next up, you will tackle the Secret Mission: adding query parameter filtering so users can fetch only completed or incomplete tasks.
Secret mission
Add Task Filtering
Your API returns every task every time. Real APIs let users filter results. Can you add a query parameter so Postman can fetch only completed or only incomplete tasks?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The taskmanager project folder on your Desktop.
- H2 in-memory database (lives only while the server is running, costs nothing).
- Spring Boot development server running locally on port 8080.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with your API.
- Your Spring Boot server continues to run in the terminal.
- You can keep sending requests in Postman to test new ideas.
- The H2 database resets each time you restart the server, so your task data is temporary by design.
Pause - I'll come back to this later
Stop the running server to free up your terminal and port 8080. Your project files stay on disk so you can resume anytime.
- Switch to the terminal where your server is running and press Ctrl+C to stop it.
- When you are ready to come back, open the taskmanager folder in VS Code and run:
.\mvnw.cmd spring-boot:run
- The H2 database starts fresh each time, so you will need to re-create any tasks through Postman.
Delete - I don't want to use this again
Remove the project folder entirely. Since everything runs locally and the database is in-memory, deleting the folder is the only cleanup needed.
- Stop the server by pressing Ctrl+C in the terminal if it is still running.
- Delete the project folder by running this command in your terminal:
cd ~/Desktop
rmdir /s /q taskmanager
- If you also want to remove the tools you installed for this project (JDK 21, Postman, VS Code extensions), you can uninstall them from Settings > Apps > Installed apps on Windows.
- VS Code extensions can be removed from the Extensions view (Ctrl+Shift+X) by right-clicking each extension and selecting Uninstall.
Nice Work!
Nice Work!
You just built a fully working REST API from scratch using Spring Boot. You can create, read, update, and delete tasks through HTTP requests, and your API handles errors gracefully instead of crashing.
You've learned how to:
- Build a complete REST API with Spring Boot that handles Create, Read, Update, and Delete operations through Postman.
- Structure a backend project using the layered architecture pattern (Controller/Service/Repository) that real production backends use to separate concerns.
- Add error handling with custom exceptions and a global exception handler, plus input validation to reject bad data before it reaches the database.
- Secret Mission: Add query parameter filtering to return only completed or incomplete tasks.
Ready to quiz yourself?