Build a Task Manager REST API

Build a Spring Boot REST API with full CRUD operations for tasks.

Introduction

30 Second Summary

Every time you open an app and see a list of items appear, that data came from a server somewhere. The server received a request, looked up the data, and sent it back as a structured response.

In this project, you will build that server. You will create a REST API with Java and Spring Boot that handles creating, reading, updating, and deleting tasks over HTTP.

What You'll Build

A Task Manager REST API that stores tasks in memory and responds to HTTP requests with JSON data.

By the end of this project, you'll have:

  • A Spring Boot project configured with the Spring Web dependency.
  • A Task model class and a TaskController class that handle full CRUD operations (Create, Read, Update, Delete).
  • Tested API endpoints using curl from your terminal to confirm everything works.
  • Secret Mission: Add filtering so clients can retrieve only completed or incomplete tasks using query parameters.

Are there any prerequisites?

This project assumes basic Java knowledge (classes, methods, and data types). No prior experience with Spring Boot or REST APIs is needed.

Set Up Your Project

Every web and mobile app you use talks to a backend server to fetch and store data. In this project, you are building that backend server using Spring Boot.

Before you write any API code, you need a project skeleton with the right dependencies. In this step, you will generate a ready-to-run Spring Boot project and confirm it starts successfully on your machine.

In this step, get ready to:
  • Install Java language support in VS Code.
  • Generate a Spring Boot project from Spring Initializr.
  • Start the server and confirm it responds on localhost.
Install the Extension Pack for Java

VS Code does not ship with Java support out of the box. The Extension Pack for Java by Microsoft adds language features like IntelliSense, debugging, and Maven integration.

  • In VS Code, open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type Extension Pack for Java in the search bar.
  • Find the result published by Microsoft and click Install.
  • Wait for the installation to finish. You may see a notification asking you to reload the window. If so, click Reload.

What does this extension pack include?

It bundles six extensions into one install: Language Support for Java (Red Hat), Debugger for Java, Test Runner for Java, Maven for Java, Gradle for Java, and Project Manager for Java.

You get code navigation, auto-completion, inline errors, and the ability to run Maven commands directly from the editor.

Generate your Spring Boot project from Spring Initializr

Spring Initializr is a web tool that generates a pre-configured Spring Boot project as a ZIP file. It saves you from manually creating the folder structure and writing a build file from scratch.

  • Open your browser and navigate to https://start.spring.io.
  • Set Project to Maven.
  • Set Language to Java.
  • Set Spring Boot to 4.0.6.
  • Under Project Metadata, set Group to com.example.
  • Set Artifact to task-api.
  • Set Name to task-api.
  • Set Packaging to Jar.
  • Set Java to 17.
  • Click Add Dependencies and search for Spring Web. Select it from the dropdown.
  • Click Generate to download the ZIP file.

What is Spring Initializr doing for you?

It creates a complete Maven project with a pom.xml build file, a main application class, and the Maven Wrapper scripts.

The Spring Web dependency gives your project everything it needs to handle HTTP requests and return JSON responses.

  • Find the downloaded task-api.zip file and extract it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the extracted task-api folder, then click Open.

VS Code may ask if you trust the authors of the folder. Click Yes, I trust the authors to continue.

Don't see the folder structure in VS Code?

Make sure you opened the task-api folder itself, not the parent folder that contains it. You should see src, pom.xml, and mvnw at the top level of the Explorer sidebar.

help me fix my project folder structure in VS Code

Start the server and confirm it responds

Your generated project includes a Maven Wrapper, which is a script that downloads and runs the correct version of Maven without you installing it globally. This means you can build and run the project immediately.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, same shortcut on both macOS and Windows).
  • Start the Spring Boot server by running this command:

macOS

./mvnw spring-boot:run

Windows

mvnw.cmd spring-boot:run

What does this command do?

The mvnw script (Maven Wrapper) downloads Maven if needed, compiles your Java code, and launches the embedded web server. The spring-boot:run part tells Maven to use the Spring Boot plugin to start the application.

The first run downloads dependencies, so it may take a minute or two. Wait until you see a line containing Started TaskApiApplication in the terminal output.

  • Open a second terminal tab by clicking the + icon in the terminal panel.
  • Test the server by running:
curl http://localhost:8080

✔️ I see a 404 or error response with JSON

That is exactly right. A 404 means the server is running and responding, but you have not defined any endpoints yet. You will add those in the next step.

ⓧ I see 'Connection refused' or no response

This means the server is not running. Switch back to your first terminal tab and check for error messages.

  • If you see a message about port 8080 already in use, another application is using that port. Stop the other application or restart your computer.
  • If you see compilation errors, make sure you selected Java 17 in Spring Initializr and that your JDK version matches.

help me fix my Spring Boot server that won't start

  • Switch back to the first terminal tab (where the server is running).
  • Press Ctrl+C to stop the server.

Why stop the server?

You will restart it after adding new code in the next step. Stopping it now frees the terminal and the port.

✔️ Awesome, I've got everything!

Great. Your Spring Boot project is generated, the server starts cleanly, and you confirmed it responds on port 8080.

ⓧ I'd like to double check the full code

Here is what your key files should look like at this point. These were auto-generated by Spring Initializr.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>4.0.6</version>
	</parent>
	<groupId>com.example</groupId>
	<artifactId>task-api</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>task-api</name>
	<description>Task Manager REST API</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>
package com.example.taskapi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TaskApiApplication {

    public static void main(String[] args) {
        SpringApplication.run(TaskApiApplication.class, args);
    }
}

Your project skeleton is ready and proven to run. Next up, you will create a Task model and your first GET endpoints so the API returns real data.

Create the Task Model and GET Endpoints

Your Spring Boot project is ready to go. The server started successfully in the last step, which means your project structure and dependencies are correctly configured.

Now it is time to build the actual functionality. Every REST API needs two things: a data model that defines the shape of information it works with, and endpoints that let clients request or modify that data. In this step, you will create both.

In this step, get ready to:
  • Create a Task model class to represent your data.
  • Create a TaskController with GET endpoints to retrieve tasks.
  • Test your endpoints using curl from the terminal.
Create the Task model class

A model class defines the shape of the data your API works with. In this case, each task has an ID, a title, and a completion status.

Spring Boot uses a library called Jackson to automatically convert Java objects to JSON when they are sent as API responses. For this to work, your model class needs getters for each field.

  • In the VS Code Explorer sidebar, navigate to src/main/java/com/example/taskapi/.
  • Right-click the taskapi folder and select New File.
  • Name the file Task.java and press Enter.
  • Copy the following code into Task.java:
package com.example.taskapi;

public class Task {

    // Each task has an ID, a title, and a completion status
    private long id;
    private String title;
    private boolean completed;

    // No-argument constructor (required by Jackson for JSON deserialization)
    public Task() {
    }

    // Constructor with all fields for convenient task creation
    public Task(long id, String title, boolean completed) {
        this.id = id;
        this.title = title;
        this.completed = completed;
    }

    // Getters and setters (Jackson uses these to convert to/from JSON)
    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;
    }
}

What does this code do?

This is a plain Java class (sometimes called a POJO) that represents a single task in your API.

  • Three private fields (id, title, completed) store the data for one task.
  • The no-argument constructor lets Jackson create Task objects from incoming JSON.
  • The getters (like getId()) let Jackson read each field when converting a Task into a JSON response.
  • Save Task.java by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Check that VS Code does not show any red underlines in the file. No red underlines means the class compiles cleanly.

Seeing red underlines?

  • Make sure the very first line is package com.example.taskapi; and matches the folder path exactly.
  • Confirm the file is saved inside src/main/java/com/example/taskapi/ and not at the project root.
  • If the Java extension is still loading, wait a few seconds for the language server to finish indexing.

Why is my Task.java showing compile errors in VS Code?

✔️ Awesome, I've got everything!

Great. Make sure you have saved Task.java before moving on.

ⓧ I'd like to double check the full code

package com.example.taskapi;

public class Task {

    private long id;
    private String title;
    private boolean completed;

    public Task() {
    }

    public Task(long id, String title, boolean completed) {
        this.id = id;
        this.title = title;
        this.completed = completed;
    }

    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;
    }
}
Create the TaskController class

A controller is the class that receives incoming HTTP requests and decides what to send back. Spring Boot uses annotations on your methods to map specific URL paths and HTTP methods to Java code.

Your TaskController will store tasks in a simple list in memory. This keeps the project focused on REST concepts without needing a database.

  • In the same taskapi folder, right-click and select New File.
  • Name the file TaskController.java and press Enter.
  • Copy the following code into TaskController.java:
package com.example.taskapi;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

// Tells Spring this class handles HTTP requests and returns data directly
@RestController
// All endpoints in this class start with /tasks
@RequestMapping("/tasks")
public class TaskController {

    // In-memory storage for tasks (no database needed)
    private final List<Task> tasks = new ArrayList<>();
    // Generates unique IDs for new tasks
    private final AtomicLong counter = new AtomicLong(1);

    // Constructor seeds the list with example tasks so GET has data to return
    public TaskController() {
        tasks.add(new Task(counter.getAndIncrement(), "Learn about REST APIs", false));
        tasks.add(new Task(counter.getAndIncrement(), "Build a Spring Boot server", false));
    }

    // GET /tasks - returns the full list of tasks as JSON
    @GetMapping
    public List<Task> getAllTasks() {
        return tasks;
    }

    // GET /tasks/{id} - returns one task by ID, or 404 if not found
    @GetMapping("/{id}")
    public ResponseEntity<Object> getTaskById(@PathVariable long id) {
        for (Task task : tasks) {
            if (task.getId() == id) {
                return ResponseEntity.ok(task);
            }
        }
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(Map.of("error", "Task not found"));
    }
}

What does this code do?

This controller defines two endpoints that clients can call to retrieve task data.

  • @RestController tells Spring this class handles HTTP requests and sends data directly in the response body (as JSON).
  • @RequestMapping("/tasks") sets the base path. Every endpoint in this class starts with /tasks.
  • @GetMapping on getAllTasks() maps GET /tasks to this method. Spring calls it whenever a client sends a GET request to that path.
  • @GetMapping("/{id}") on getTaskById() maps GET /tasks/1, GET /tasks/2, etc. The @PathVariable annotation pulls the number from the URL into the id parameter.
  • ResponseEntity lets you control the HTTP status code. If the task is not found, the method returns a 404 status with an error message.

Why in-memory storage instead of a database?

The goal of this project is to learn how REST endpoints and HTTP methods work. Adding a database would introduce connection strings, schemas, and setup complexity that distracts from the core concept.

The simple ArrayList stores tasks in memory while the server is running. The data resets every time you restart the server, which is perfectly fine for learning.

  • Save TaskController.java by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Check that VS Code does not show any red underlines in the file.

Red underlines on the import statements?

  • Make sure Task.java is saved in the same taskapi folder. The controller references the Task class directly.
  • If you see errors on the Spring import lines, the Java extension may still be downloading dependencies. Wait 10-20 seconds for it to finish.
  • Confirm the file is named exactly TaskController.java with a capital T and capital C.

Why is TaskController.java showing import errors in VS Code?

✔️ Awesome, I've got everything!

Great. Make sure you have saved TaskController.java before moving on.

ⓧ I'd like to double check the full code

package com.example.taskapi;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/tasks")
public class TaskController {

    private final List<Task> tasks = new ArrayList<>();
    private final AtomicLong counter = new AtomicLong(1);

    public TaskController() {
        tasks.add(new Task(counter.getAndIncrement(), "Learn about REST APIs", false));
        tasks.add(new Task(counter.getAndIncrement(), "Build a Spring Boot server", false));
    }

    @GetMapping
    public List<Task> getAllTasks() {
        return tasks;
    }

    @GetMapping("/{id}")
    public ResponseEntity<Object> getTaskById(@PathVariable long id) {
        for (Task task : tasks) {
            if (task.getId() == id) {
                return ResponseEntity.ok(task);
            }
        }
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(Map.of("error", "Task not found"));
    }
}
Start the server and test your endpoints

Both files are saved. Time to start the server and prove that your endpoints respond with real JSON data.

  • Start the Spring Boot server by running this command in your terminal:
./mvnw spring-boot:run
  • Wait for the Spring Boot banner to appear and the line that says Started TaskApiApplication. This confirms the server is running.

Server fails to start?

  • Read the error message in the terminal. If it mentions a compilation error in Task.java or TaskController.java, double-check your code against the full-code tabs above.
  • If the error says port 8080 is already in use, you may have a previous server still running. Press Ctrl+C in all terminal tabs to stop any existing processes, then try again.

Why is my Spring Boot server failing to start?

Now open a second terminal tab so you can send requests while the server keeps running.

  • Click the + icon in the VS Code terminal panel to open a new terminal tab.
  • Retrieve all tasks by running:
curl http://localhost:8080/tasks

You should see a JSON array with two tasks printed in your terminal.

What should I see?

The response looks something like this:

[{"id":1,"title":"Learn about REST APIs","completed":false},{"id":2,"title":"Build a Spring Boot server","completed":false}]
  • Retrieve a single task by ID by running:
curl http://localhost:8080/tasks/1

You should see a single JSON object for the task with ID 1.

  • Test the 404 response for a task that does not exist by running:
curl http://localhost:8080/tasks/99

You should see an error response with the message "Task not found". This confirms your controller correctly returns a 404 status when no matching task exists.

Getting connection refused?

  • Make sure your server is still running in the first terminal tab. Look for the Started TaskApiApplication message.
  • Confirm you are running the curl commands in the second terminal tab, not the one running the server.
  • Check that you typed localhost:8080 exactly. A typo in the port number will fail to connect.

Why am I getting connection refused when I run curl against localhost?

Your API can now serve task data over HTTP. Next up, you will add the ability to create, update, and delete tasks to complete the full set of CRUD operations.

Add Create, Update, and Delete Endpoints

In the previous step, you built GET endpoints that let clients read tasks from your API. Reading data is only half the story.

A real API also needs to create new tasks, update existing ones, and delete tasks that are no longer needed. These operations map to three HTTP methods: POST, PUT, and DELETE. In this step, you will add all three to your controller.

In this step, get ready to:
  • Add a POST endpoint that creates new tasks.
  • Add PUT and DELETE endpoints that update and remove tasks.
  • Test all endpoints with curl to confirm full CRUD functionality.
Add the POST endpoint to create tasks

When a client sends a POST request, it includes a JSON body with the new task's data. Your endpoint needs to read that body, assign the task a unique ID, and return the newly created task.

  • In TaskController.java, find the import statements at the top of the file. Add these four new imports below the existing ones:
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
  • Scroll down to after the closing } of the getTaskById method. Add the create method below it:
    @PostMapping
    public ResponseEntity<Task> createTask(@RequestBody Task task) {
        // Assign a unique ID and default completed to false
        task.setId(counter.getAndIncrement());
        task.setCompleted(false);
        tasks.add(task);
        // Return the new task with HTTP 201 Created
        return ResponseEntity.status(HttpStatus.CREATED).body(task);
    }

What does this code do?

`@PostMapping` maps HTTP POST requests to this method. Since the class already has `@RequestMapping("/tasks")`, this method handles POST requests to `/tasks`.

`@RequestBody Task task` tells Spring to read the incoming JSON body and convert it into a `Task` object automatically. Spring uses a library called Jackson to handle this conversion behind the scenes.

`ResponseEntity.status(HttpStatus.CREATED)` sends back HTTP status code 201, which is the standard response for "a new resource was successfully created."

  • Save TaskController.java.
  • You will not see a change yet. You will test this endpoint after adding the remaining methods.

Seeing a red underline on @PostMapping or @RequestBody?

Make sure you added the new import statements at the top of the file. Without the imports, Java does not know where these annotations come from.

Check that the import lines match exactly: import org.springframework.web.bind.annotation.PostMapping; and import org.springframework.web.bind.annotation.RequestBody;.

Help me fix the red underline in my TaskController

Add the PUT and DELETE endpoints

The PUT method updates an existing task by finding it by ID and replacing its values. The DELETE method removes a task entirely.

  • Below the createTask method you just added, add the update method:
    @PutMapping("/{id}")
    public ResponseEntity<Object> updateTask(@PathVariable long id, @RequestBody Task updatedTask) {
        // Find the task with the matching ID
        for (Task task : tasks) {
            if (task.getId() == id) {
                // Update the task's fields from the request body
                task.setTitle(updatedTask.getTitle());
                task.setCompleted(updatedTask.isCompleted());
                return ResponseEntity.ok(task);
            }
        }
        // Return 404 if no task matches the given ID
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(Map.of("error", "Task not found"));
    }

What does this code do?

`@PutMapping("/{id}")` maps HTTP PUT requests to `/tasks/{id}`. The `{id}` portion is a path variable, meaning the client specifies which task to update in the URL itself.

The method loops through the list to find the matching task. If found, it updates the title and completed status from the request body, then returns the updated task with a 200 OK response.

If no task matches, it returns a 404 Not Found response with an error message. This follows the same pattern you used in getTaskById.

  • Below the updateTask method, add the delete method:
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteTask(@PathVariable long id) {
        // Remove the task if it exists
        boolean removed = tasks.removeIf(t -> t.getId() == id);
        if (removed) {
            // Return 204 No Content on successful deletion
            return ResponseEntity.noContent().build();
        }
        // Return 404 if the task was not found
        return ResponseEntity.notFound().build();
    }

What does this code do?

`@DeleteMapping("/{id}")` maps HTTP DELETE requests to `/tasks/{id}`.

`tasks.removeIf(t -> t.getId() == id)` uses a lambda expression to find and remove the task in one step. It returns true if a task was removed, false if none matched.

On success, the method returns HTTP 204 (No Content). This status code means "the action was successful, but there is no response body to send back." On failure, it returns 404.

  • Save TaskController.java.
  • You will test all three new endpoints in the next substep.

Seeing compilation errors?

Make sure each method is placed inside the TaskController class (between the class opening { and the final closing }). Methods placed outside the class will not compile.

Check that you have not accidentally deleted the closing brace of the getTaskById method above.

Help me fix the compilation errors in my TaskController

✔️ Awesome, I've got everything!

Great. Double check you have saved TaskController.java before moving on.

ⓧ I'd like to double check the full code

Here is the complete TaskController.java file with all the changes from this step:

package com.example.taskapi;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/tasks")
public class TaskController {

    private final List<Task> tasks = new ArrayList<>();
    private final AtomicLong counter = new AtomicLong(1);

    public TaskController() {
        tasks.add(new Task(counter.getAndIncrement(), "Learn about REST APIs", false));
        tasks.add(new Task(counter.getAndIncrement(), "Build a Spring Boot server", false));
    }

    @GetMapping
    public List<Task> getAllTasks() {
        return tasks;
    }

    @GetMapping("/{id}")
    public ResponseEntity<Object> getTaskById(@PathVariable long id) {
        for (Task task : tasks) {
            if (task.getId() == id) {
                return ResponseEntity.ok(task);
            }
        }
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(Map.of("error", "Task not found"));
    }

    @PostMapping
    public ResponseEntity<Task> createTask(@RequestBody Task task) {
        task.setId(counter.getAndIncrement());
        task.setCompleted(false);
        tasks.add(task);
        return ResponseEntity.status(HttpStatus.CREATED).body(task);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Object> updateTask(@PathVariable long id, @RequestBody Task updatedTask) {
        for (Task task : tasks) {
            if (task.getId() == id) {
                task.setTitle(updatedTask.getTitle());
                task.setCompleted(updatedTask.isCompleted());
                return ResponseEntity.ok(task);
            }
        }
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(Map.of("error", "Task not found"));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteTask(@PathVariable long id) {
        boolean removed = tasks.removeIf(t -> t.getId() == id);
        if (removed) {
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }
}
Restart the server and test all endpoints

Your controller now has full CRUD capabilities. Time to restart the server and confirm each endpoint works as expected.

  • If the server is still running from earlier, press Ctrl+C in the terminal to stop it.
  • Restart the server by running:
./mvnw spring-boot:run
  • Wait for the Started TaskApiApplication message to appear.
  • Open a new terminal tab (Ctrl+Shift+` in VS Code).
  • Test the POST endpoint by creating a new task:
curl -X POST http://localhost:8080/tasks -H "Content-Type: application/json" -d '{"title":"Buy groceries"}'

You should see a JSON response with the new task, including an auto-assigned ID and completed set to false.

  • Test the PUT endpoint by updating an existing task:
curl -X PUT http://localhost:8080/tasks/1 -H "Content-Type: application/json" -d '{"title":"Learn REST APIs","completed":true}'

You should see the updated task with completed now set to true.

  • Test the DELETE endpoint by removing a task:
curl -X DELETE http://localhost:8080/tasks/1

This command returns no body (HTTP 204 means success with no content). Confirm the task was removed by listing all tasks:

  • Verify the deletion worked by running:
curl http://localhost:8080/tasks

You should see that task 1 is no longer in the list. The remaining tasks and any new ones you created via POST should still appear.

  • Test a 404 response by requesting a task ID that does not exist:
curl -X DELETE http://localhost:8080/tasks/99

You should see an empty response since the task was not found (the server returns a 404 status).

What are HTTP status codes?

Status codes are three-digit numbers the server sends back to tell the client what happened. Here are the ones your API uses:

  • 200 OK: the request was successful and the response body contains data.
  • 201 Created: a new resource was successfully created.
  • 204 No Content: the action succeeded but there is nothing to send back.
  • 404 Not Found: the requested resource does not exist.

Getting "Connection refused" when running curl?

Make sure the server is running in another terminal tab. Look for the Started TaskApiApplication message before sending curl commands.

If the server failed to start, check for compilation errors in the first terminal. A missing brace or typo in the annotations will prevent Spring Boot from starting.

Help me troubleshoot why my Spring Boot server won't start

Your API now supports full CRUD: Create, Read, Update, and Delete. Every modern backend service is built on these same four operations. Next up, you will take on a secret mission to add even more power to your API.

Secret mission

Add Filtering to Your API

Your API returns all tasks every time, but real-world clients need to ask for specific subsets. Can you add query parameter filtering so clients can request only completed or 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 task-api project folder on your Desktop.
  • Extension Pack for Java (VS Code extension).
  • The Spring Boot development server (if still running).

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend your API with more features.

  • Your task-api folder stays on your Desktop.
  • The Extension Pack for Java remains installed in VS Code, ready for your next Java project.
  • If the server is still running, you can stop it anytime with Ctrl+C in the terminal.

Pause - I'll come back to this later

Shut down the running server process to free up your terminal. Your project files stay on disk for when you return.

  • If the Spring Boot server is still running, press Ctrl+C in the terminal where you ran ./mvnw spring-boot:run.
  • Your task-api folder and Extension Pack for Java stay in place.
  • To resume later, open the task-api folder in VS Code and run ./mvnw spring-boot:run again.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your code and uninstalls the VS Code extension.

  • If the Spring Boot server is still running, press Ctrl+C in the terminal to stop it.
  • Delete the project folder. On macOS/Linux, run:
rm -rf ~/Desktop/task-api

On Windows, run:

rmdir /s /q %USERPROFILE%\Desktop\task-api
  • Uninstall the Extension Pack for Java in VS Code. Open the Extensions sidebar (Cmd+Shift+X on macOS or Ctrl+Shift+X on Windows), find Extension Pack for Java, click it, then click Uninstall.

Nice Work!

Nice Work!

You just built a fully functional REST API server from scratch using Java and Spring Boot. This is the same architecture that powers backends at companies of every size.

Here's what you accomplished:

  • Built a REST API server with full CRUD operations (Create, Read, Update, Delete) that stores and manages tasks in memory.
  • Used Spring Boot annotations (@RestController, @GetMapping, @PostMapping, @PutMapping, @DeleteMapping) to map HTTP requests directly to Java methods.
  • Returned appropriate HTTP status codes (200, 201, 204, 404) using ResponseEntity and tested every endpoint with curl from the terminal.
  • Secret Mission: Added query parameter filtering using @RequestParam and Java streams to return tasks by completion status.

Ready to quiz yourself?