Build a REST API with Java HTTP Server

Build a Task Manager REST API using only Java's built-in HTTP server.

Introduction

30 Second Summary

Every time you check the weather, scroll social media, or order food online, your device is sending HTTP requests to a server and receiving responses. That's the core of how the internet works.

In this project, you will build a Task Manager REST API using only Java's built-in HTTP server. No frameworks, no build tools, no external dependencies.

What You'll Build

A fully functional REST API that handles creating, listing, and deleting tasks through HTTP requests, built with nothing but Java's standard library.

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

  • A running HTTP server built entirely with Java's standard library (zero external dependencies).
  • A REST API with GET, POST, and DELETE endpoints that manages a list of tasks in memory.
  • Hands-on experience with HTTP fundamentals: methods, status codes, headers, and request/response bodies.
  • Secret Mission: Add a PUT endpoint to update existing tasks and query parameter filtering to search tasks by status.

Are there any prerequisites?

You should be comfortable with basic Java concepts like variables, classes, methods, and collections. No web development or HTTP experience is needed.

Before We Start

Before you can build a REST API, you need a working Java development environment.

In this step, you will install JDK 21, set up your code editor, and confirm everything works by compiling a simple test file.

In this step, get ready to:
  • Install JDK 21 and verify it works.
  • Set up Cursor (or VS Code) with a new project folder.
  • Compile and run your first Java file.
Download and install JDK 21

The JDK (Java Development Kit) includes the compiler and runtime you need to write and run Java programs.

  • Check whether Java is already installed by running this command in your terminal:
java --version

✔️ I see version 21 or higher

Your output looks something like java 21.0.11. You are all set.

  • Confirm the compiler is also available by running:
javac --version

You should see a matching version number starting with 21. Skip ahead to the next substep.

ⓧ I see an older version

If you see a version lower than 21 (for example 17.0.x or 11.0.x), you need to install JDK 21. The new installer will replace your current version.

  • Open https://www.oracle.com/java/technologies/downloads/#java21 in your browser.
  • Select the installer for your operating system (Windows x64 Installer, or macOS DMG Installer).
  • Run the installer and accept the default options.
  • Close your terminal and open a fresh one so the new version takes effect.
  • Verify the new version by running:
java --version

You should now see java 21.0.11 or similar.

ⓧ Command not found

Java is not installed on your machine yet. You will download it from Oracle.

  • Open https://www.oracle.com/java/technologies/downloads/#java21 in your browser.
  • Select the installer for your operating system (Windows x64 Installer, or macOS DMG Installer).
  • Run the installer and accept the default options.
  • Close your terminal and open a fresh one so the system recognises the new installation.
  • Verify the installation by running:
java --version

You should see output starting with java 21.0.11.

  • Also confirm the compiler is available by running:
javac --version

You should see a version number starting with 21.

Still seeing an old version after installing?

Make sure you opened a brand-new terminal window after the installer finished. Existing terminals cache the old path.

On macOS, if java --version still shows the old version, run /usr/libexec/java_home -V to list installed JDKs and confirm 21 is present.

Help me fix my Java version issue

Open your editor and create the project folder
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor (or VS Code if you prefer Visual Studio Code) and press Enter to open it.

Now create your project folder on the Desktop so it is easy to find later.

  • Open a terminal. Press Ctrl+` inside Cursor or VS Code to open the integrated terminal.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir task-api
cd task-api
  • Open this folder in your editor by running:
code .

Using Cursor instead of VS Code?

If you are using Cursor, replace code . with cursor .. Both commands open the current folder in the editor.

  • Create a new file called TaskServer.java inside the task-api folder. In Cursor or VS Code, click File in the top menu bar.
  • Click New File.
  • Type TaskServer.java as the file name and press Enter to save it.

Why the file name must match the class name

In Java, the public class name and the file name must match exactly. Your class will be called TaskServer, so the file must be TaskServer.java.

Compile and run a test file

A quick compile-and-run cycle confirms that your JDK, editor, and terminal are all connected correctly.

  • Paste the following code into TaskServer.java:
// Main class - must match the filename TaskServer.java
public class TaskServer {
    // Entry point - Java runs this method when you start the program
    public static void main(String[] args) {
        System.out.println("Ready!");
    }
}

What does this code do?

public class TaskServer declares the class. Every Java program needs at least one class.

public static void main(String[] args) is the entry point. When you run java TaskServer, the JVM looks for this exact method signature and executes it.

System.out.println("Ready!") prints text to the terminal so you can confirm the program ran.

  • Save TaskServer.java with Cmd+S (macOS) or Ctrl+S (Windows).
  • In the integrated terminal, make sure you are inside the task-api folder. Compile the file by running:
javac TaskServer.java
  • Run the compiled program:
java TaskServer

You should see Ready! printed in your terminal.

Seeing an error instead of Ready?

If you see error: class TaskServer is public, should be declared in a file named TaskServer.java, check that your file is named exactly TaskServer.java with a capital T and capital S.

If you see javac: file not found, confirm that your terminal is inside the task-api folder by running ls and checking that TaskServer.java appears in the listing.

Help me fix my Java compile error

✔️ Awesome, I've got everything!

Great. Make sure you have saved TaskServer.java and seen Ready! in your terminal output.

ⓧ I'd like to double check the full code

public class TaskServer {
    public static void main(String[] args) {
        System.out.println("Ready!");
    }
}

Your Java environment is working perfectly. Next up, you will build an actual HTTP server that listens for requests on port 8080.

Create a Basic HTTP Server

Your Java environment is ready and you have confirmed it compiles. Now it is time to turn that simple class into an actual HTTP server that can receive requests and send responses.

Java ships with a built-in HTTP server class called HttpServer in the com.sun.net.httpserver package. You will use it to listen for incoming requests on port 8080 and send responses back, with zero external dependencies.

In this step, get ready to:
  • Create an HTTP server bound to port 8080.
  • Test the server with curl from a second terminal.
  • Set the Content-Type response header.
Import the server classes and create the handler

The HttpServer class has been part of the JDK since Java 1.6. It handles the low-level TCP connection and HTTP parsing for you, but leaves routing and response construction entirely in your hands.

  • Replace the entire contents of TaskServer.java with the following code:
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

public class TaskServer {

    public static void main(String[] args) throws IOException {
        // Create an HTTP server listening on port 8080
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);

        // Register a handler for the root path
        server.createContext("/", exchange -> {
            String response = "Task Manager API is running!";
            byte[] responseBytes = response.getBytes("UTF-8");
            exchange.sendResponseHeaders(200, responseBytes.length);
            OutputStream os = exchange.getResponseBody();
            os.write(responseBytes);
            os.close();
        });

        // Use default executor and start the server
        server.setExecutor(null);
        server.start();
        System.out.println("Server started on http://localhost:8080");
    }
}

What does this code do?

HttpServer.create(new InetSocketAddress(8080), 0) creates a server that listens on port 8080. The 0 is the TCP backlog (system default).

server.createContext("/", exchange -> {...}) maps the root path / to a lambda handler. Every request to / triggers this handler.

server.setExecutor(null) tells the server to use a default single-threaded executor. server.start() launches the server in a background thread, keeping the program alive.

  • Save TaskServer.java.

Seeing red squiggles in your editor?

Your editor might show warnings about com.sun.net.httpserver being a restricted API. This is safe to ignore. The HTTP server package is officially exported for external use since JDK 17 (JEP 403).

help me understand the com.sun.net.httpserver warnings

Compile and run the server
  • Compile your server by running this command in the terminal:
javac TaskServer.java
  • Start the server by running:
java TaskServer

You should see Server started on http://localhost:8080 printed in the terminal. The program keeps running because the server is listening for requests.

Why does the terminal hang?

The terminal is not frozen. Your server is actively waiting for HTTP requests. It will stay running until you press Ctrl+C to stop it.

  • Open a second terminal window by pressing Ctrl+Shift+` in Cursor (or VS Code) to open a new terminal tab.
  • Test your server by running this command in the new terminal:
curl http://localhost:8080/

You should see Task Manager API is running! printed as the response.

What just happened?

You just completed a full HTTP request/response cycle. curl sent an HTTP GET request to localhost:8080. Your server received it, ran the handler, and sent back the text response with a 200 status code.

Connection refused error?

  • Make sure the first terminal still shows the server running. If you see the command prompt again, the server has stopped.
  • Check that you typed the URL exactly as http://localhost:8080/ with the trailing slash.
  • If another program is using port 8080, you will see an Address already in use error when starting the server. Close the other program first.

help me fix a connection refused error with my Java HTTP server

Set the Content-Type response header

Your server works, but it is not telling the client what kind of data it is sending back. HTTP responses use headers to communicate metadata about the response body.

  • In the first terminal, press Ctrl+C to stop the running server.
  • In TaskServer.java, find these two lines inside the handler:
            String response = "Task Manager API is running!";
            byte[] responseBytes = response.getBytes("UTF-8");
  • Add this new line between them, so the header is set before you calculate the response bytes:
            String response = "Task Manager API is running!";
            exchange.getResponseHeaders().set("Content-Type", "text/plain");
            byte[] responseBytes = response.getBytes("UTF-8");

What is the response lifecycle?

Every HTTP response follows a strict order. You must follow these four steps in sequence:

  • Set headers using exchange.getResponseHeaders().set(...). Headers must be set before sending anything else.
  • Send response headers using sendResponseHeaders(200, responseBytes.length). This commits the status code and headers to the client.
  • Write the body using getResponseBody() and writing bytes to the output stream.
  • Close the stream. This signals that the response is complete.
  • Save TaskServer.java.
  • Recompile and restart the server by running these commands in the first terminal:
javac TaskServer.java
java TaskServer
  • Switch to your second terminal and run the curl command again:
curl http://localhost:8080/

You should see the same Task Manager API is running! response. The visible output is the same, but now your server properly declares the content type.

Inspect headers with curl -v

To see the full HTTP response including headers, run curl -v http://localhost:8080/ instead. You will see the Content-Type: text/plain header in the response section.

Server won't start after recompiling?

  • Make sure you stopped the previous server with Ctrl+C before running java TaskServer again. Two servers cannot use the same port.
  • If you see a compilation error, check that the new exchange.getResponseHeaders().set("Content-Type", "text/plain") line is inside the lambda (between the curly braces) and ends with a semicolon.

help me fix a port conflict when restarting my Java HTTP server

✔️ Awesome, I've got everything!

Great. Double check you have saved TaskServer.java and your server is still running.

ⓧ I'd like to double check the full code

Here is the complete TaskServer.java file at the end of this step:

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

public class TaskServer {

    public static void main(String[] args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/", exchange -> {
            String response = "Task Manager API is running!";
            exchange.getResponseHeaders().set("Content-Type", "text/plain");
            byte[] responseBytes = response.getBytes("UTF-8");
            exchange.sendResponseHeaders(200, responseBytes.length);
            OutputStream os = exchange.getResponseBody();
            os.write(responseBytes);
            os.close();
        });
        server.setExecutor(null);
        server.start();
        System.out.println("Server started on http://localhost:8080");
    }
}

Your server is alive and responding to requests. Next up, you will build a proper data model for tasks and store them in memory so your API has something real to manage.

Build the Task Data Model and In-Memory Storage

Your server is running and responding to requests. Now it needs something to serve.

A REST API manages data. In this step, you will create a Task data model and store tasks in memory so your API has something to return when clients ask for them.

In this step, get ready to:
  • Create a Task class with a JSON output method.
  • Add in-memory storage with seed data.
  • Build a helper method that converts the task list to a JSON array.
Create the Task inner class

Every task needs a structure: an ID, a title, and a status. In Java, you define this structure as a class.

  • In TaskServer.java, find the opening line of the TaskServer class:
public class TaskServer {
  • Add the following Task class inside TaskServer, just below the opening brace of public class TaskServer { and above the main method:
    static class Task {
        int id;
        String title;
        String status;

        Task(int id, String title, String status) {
            this.id = id;
            this.title = title;
            this.status = status;
        }

        // Converts this task into a JSON-formatted string
        String toJson() {
            return "{\"id\":" + id + ",\"title\":\"" + title + "\",\"status\":\"" + status + "\"}";
        }
    }

What does this code do?

This is a static inner class, meaning it lives inside TaskServer but doesn't need an instance of the outer class to exist.

  • id, title, and status are the three pieces of data every task holds.
  • The constructor sets all three fields when you create a new Task.
  • toJson() manually builds a JSON string by concatenating the field values with the correct braces and quotes.

Why build JSON by hand?

Production Java projects use libraries like Jackson or Gson to serialize objects to JSON. This project avoids external dependencies on purpose so you can see exactly what JSON looks like at the string level.

  • Save TaskServer.java.
  • Compile by running:
javac TaskServer.java
  • You should see no errors. If it compiles silently (no output), your Task class syntax is correct.

Seeing a compilation error?

  • Check that the Task class is placed inside the TaskServer class (between its opening and closing braces), not outside it.
  • Make sure every opening brace { has a matching closing brace }. The toJson() method has many escaped quotes. Copy it carefully.

Still stuck? Help me fix my Task class compilation error

Add in-memory storage and seed data

Your Task class defines the shape of a task. Now you need a place to keep tasks and some starter data to work with.

  • Add this import at the top of the file, below the existing imports:
import java.util.ArrayList;
  • Inside the TaskServer class, add these two static fields directly below public class TaskServer { and above the Task inner class:
    // In-memory list to store all tasks
    static ArrayList<Task> tasks = new ArrayList<>();
    // Counter to assign unique IDs to new tasks
    static int nextId = 1;
  • Inside the main method, add these two lines at the very top (before the HttpServer.create line):
        // Seed the task list with sample data
        tasks.add(new Task(nextId++, "Learn Java HTTP", "in-progress"));
        tasks.add(new Task(nextId++, "Build REST API", "todo"));

What does this code do?

  • tasks is an ArrayList that acts as your database. Since it lives in memory, all data resets when the server restarts.
  • nextId is a counter that auto-increments every time you create a task. The ++ operator uses the current value and then bumps it up by one.
  • The two tasks.add() lines give you starter data so you can immediately test GET requests in the next step.
  • Save TaskServer.java and compile again by running:
javac TaskServer.java
  • You should see no errors. This confirms your import, fields, and seed data are all wired up correctly.

Seeing 'cannot find symbol' for Task?

  • Make sure your static ArrayList<Task> tasks line is placed inside the TaskServer class, not outside it or inside the main method.
  • Check that import java.util.ArrayList; appears at the top of the file with the other imports.

Need more help? Help me fix this compilation error

Create the tasksToJson helper method

When a client asks for the task list, the server needs to return all tasks as a JSON array. A helper method will handle this conversion.

  • Add the following method inside the TaskServer class, below the Task inner class and above the main method:
    // Converts the full task list into a JSON array string
    static String tasksToJson(ArrayList<Task> taskList) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < taskList.size(); i++) {
            sb.append(taskList.get(i).toJson());
            if (i < taskList.size() - 1) {
                sb.append(",");
            }
        }
        sb.append("]");
        return sb.toString();
    }

What does this code do?

  • StringBuilder efficiently builds a string piece by piece without creating many intermediate string objects.
  • The loop calls toJson() on each task and appends a comma between items (but not after the last one).
  • The result is a valid JSON array like [{"id":1,...},{"id":2,...}].
  • Save TaskServer.java.
  • Compile and run the server to confirm everything works together:
javac TaskServer.java
java TaskServer
  • You should see Server started on http://localhost:8080 printed in the terminal. This confirms all your new code compiles and the server starts without errors.
  • Stop the server with Ctrl+C when you are done verifying.

Server not starting?

  • If you see an error about port 8080 already in use, make sure you stopped the previous server instance with Ctrl+C before running again.
  • If compilation fails, check that tasksToJson is placed inside the TaskServer class but outside the Task inner class and outside the main method.

Still stuck? Help me debug why my TaskServer won't compile or start

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

Here is the complete TaskServer.java file as it should look at the end of this step.

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.ArrayList;

public class TaskServer {

    static ArrayList<Task> tasks = new ArrayList<>();
    static int nextId = 1;

    static class Task {
        int id;
        String title;
        String status;

        Task(int id, String title, String status) {
            this.id = id;
            this.title = title;
            this.status = status;
        }

        String toJson() {
            return "{\"id\":" + id + ",\"title\":\"" + title + "\",\"status\":\"" + status + "\"}";
        }
    }

    static String tasksToJson(ArrayList<Task> taskList) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < taskList.size(); i++) {
            sb.append(taskList.get(i).toJson());
            if (i < taskList.size() - 1) {
                sb.append(",");
            }
        }
        sb.append("]");
        return sb.toString();
    }

    public static void main(String[] args) throws IOException {
        tasks.add(new Task(nextId++, "Learn Java HTTP", "in-progress"));
        tasks.add(new Task(nextId++, "Build REST API", "todo"));

        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/", exchange -> {
            String response = "Task Manager API is running!";
            exchange.getResponseHeaders().set("Content-Type", "text/plain");
            byte[] responseBytes = response.getBytes("UTF-8");
            exchange.sendResponseHeaders(200, responseBytes.length);
            OutputStream os = exchange.getResponseBody();
            os.write(responseBytes);
            os.close();
        });
        server.setExecutor(null);
        server.start();
        System.out.println("Server started on http://localhost:8080");
    }
}

Your data model is in place and loaded with sample tasks. Next up, you will wire the /tasks endpoint to serve this data as JSON when clients send GET and POST requests.

Implement GET and POST Endpoints

Your server is running, but it only returns a plain text message at the root path. A real REST API needs to respond differently depending on what the client is asking for.

In this step, you will register a /tasks endpoint that routes requests by their HTTP method. GET requests will return the full task list as JSON. POST requests will accept a new task in the request body, add it to the list, and return the created task.

In this step, get ready to:
  • Route GET and POST requests to separate handler methods.
  • Return the task list as JSON on GET requests.
  • Create new tasks from POST request bodies.
Replace the root handler with a /tasks endpoint

The root handler you built earlier was a proof of concept. Now you will replace it with a proper endpoint that responds at /tasks and knows how to handle different HTTP methods.

First, you need a reusable helper method for sending JSON responses. Every endpoint will set headers, write bytes, and close the stream. Extracting this into a method keeps your handler code clean.

  • In TaskServer.java, find the import block at the top of the file. Add these new imports below the existing ones:
import java.io.InputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.stream.Collectors;

Why these imports?

InputStream, BufferedReader, and InputStreamReader are for reading the request body when clients send data (POST requests). Collectors lets you join stream lines into a single string.

  • Add the sendResponse helper method below the tasksToJson method by copying this code:
    static void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException {
        // Set the response content type to JSON
        exchange.getResponseHeaders().set("Content-Type", "application/json");
        // Convert the response string to bytes
        byte[] responseBytes = response.getBytes("UTF-8");
        // Send status code and content length, then write the body
        exchange.sendResponseHeaders(statusCode, responseBytes.length);
        OutputStream os = exchange.getResponseBody();
        os.write(responseBytes);
        os.close();
    }

What does this code do?

sendResponse wraps the three-step response lifecycle into one call: set the Content-Type header, send the status code with the byte length, then write and close the body stream.

Every endpoint reuses this helper, so you only write the response logic once.

You won't see a change yet. The next chunk uses this helper inside the new handler class.

  • Now create the TaskHandler class at the bottom of TaskServer.java, after the closing brace of the main method but still inside the TaskServer class. Copy this code:
    static class TaskHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            // Check which HTTP method the client used
            String method = exchange.getRequestMethod();
            switch (method) {
                case "GET":
                    handleGet(exchange);
                    break;
                case "POST":
                    handlePost(exchange);
                    break;
                default:
                    // Reject unsupported methods with 405
                    sendResponse(exchange, 405, "{\"error\":\"Method not allowed\"}");
                    break;
            }
        }

        void handleGet(HttpExchange exchange) throws IOException {
            // Return the full task list as JSON
            String response = tasksToJson(tasks);
            sendResponse(exchange, 200, response);
        }

        void handlePost(HttpExchange exchange) throws IOException {
            // Placeholder - we will implement this next
        }
    }

What does this code do?

TaskHandler implements HttpHandler, which requires a handle() method. This is the single entry point for every request to /tasks.

The switch statement reads the HTTP method and routes to the right handler function. handleGet uses your existing tasksToJson helper to return all tasks with a 200 status code.

  • Now update the main method to use the new handler. Find this line inside main:
        server.createContext("/", exchange -> {
  • Delete the entire old lambda handler (from server.createContext("/", exchange -> { through its closing });) and replace it with these lines:
        server.createContext("/tasks", new TaskHandler());
        server.setExecutor(null);
        server.start();
        System.out.println("Server started on http://localhost:8080");
        System.out.println("Try: curl http://localhost:8080/tasks");

What changed in main?

Instead of an inline lambda at /, you now register TaskHandler at /tasks. The HttpServer routes all requests starting with /tasks (including /tasks/1) to this single handler.

  • Save TaskServer.java.
  • If you have the server still running from earlier, stop it by pressing Ctrl+C in that terminal.
  • Compile and run the server by running these commands:
javac TaskServer.java
java TaskServer
  • Open a second terminal. Test the GET endpoint by running:
curl http://localhost:8080/tasks

You should see the two seed tasks returned as a JSON array.

Seeing "cannot find symbol" errors?

Make sure you added all four new import statements at the top of the file. If javac reports a missing symbol, check that java.io.InputStream, java.io.BufferedReader, java.io.InputStreamReader, and java.util.stream.Collectors are all present.

Also confirm that TaskHandler is inside the TaskServer class (before its final closing brace), not outside it.

Help me debug my TaskHandler compilation errors

Add POST support to create new tasks

GET works. Now you need POST, which is more complex because the server has to read data from the request body. You will add two parsing helpers, then wire them into the handlePost method.

  • Add the readRequestBody helper method. Place it below the tasksToJson method (and above sendResponse):
    static String readRequestBody(HttpExchange exchange) throws IOException {
        // Get the raw input stream from the request
        InputStream is = exchange.getRequestBody();
        // Wrap it in a BufferedReader for line-by-line reading
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        // Join all lines into a single string
        String body = reader.lines().collect(Collectors.joining());
        reader.close();
        return body;
    }

What does this code do?

exchange.getRequestBody() returns a raw InputStream of bytes. You wrap it in an InputStreamReader (bytes to characters) and then a BufferedReader (characters to lines). Finally, Collectors.joining() merges all lines into one string you can parse.

You won't see a change yet. The next helper extracts values from the JSON body.

  • Add the extractJsonValue helper method directly below readRequestBody:
    static String extractJsonValue(String json, String key) {
        // Build the search pattern for "key":"value"
        String searchKey = "\"" + key + "\":\"";
        int startIndex = json.indexOf(searchKey);
        if (startIndex == -1) return null;
        // Move past the key pattern to the start of the value
        startIndex += searchKey.length();
        int endIndex = json.indexOf("\"", startIndex);
        if (endIndex == -1) return null;
        // Extract the value between the quotes
        return json.substring(startIndex, endIndex);
    }

What does this code do?

This is a simple JSON parser that finds a key-value pair by searching for the pattern "key":"value" in the raw string. It returns just the value portion, or null if the key is not found.

Production code would use a JSON library. For learning HTTP fundamentals, manual parsing keeps the focus on the request/response cycle.

You won't see a change yet. The next chunk uses both helpers inside handlePost.

  • Find the empty handlePost method inside TaskHandler and replace it with this implementation:
        void handlePost(HttpExchange exchange) throws IOException {
            // Read the JSON body from the request
            String body = readRequestBody(exchange);
            // Extract the "title" field from the JSON
            String title = extractJsonValue(body, "title");
            // Validate that a title was provided
            if (title == null || title.isEmpty()) {
                sendResponse(exchange, 400, "{\"error\":\"Title is required\"}");
                return;
            }
            // Create a new task with auto-incremented ID
            Task newTask = new Task(nextId++, title, "todo");
            tasks.add(newTask);
            // Return the created task with 201 Created status
            sendResponse(exchange, 201, newTask.toJson());
        }

What does this code do?

handlePost reads the request body, extracts the title field, and validates it. If the title is missing, it returns a 400 (Bad Request) error.

When the title is valid, it creates a new Task with status todo and an auto-incremented ID. The response uses 201 (Created), which is the standard HTTP status code for successful resource creation.

  • Save TaskServer.java.

Compilation errors in handlePost?

If you see an error about readRequestBody or extractJsonValue, make sure both helper methods are static and placed inside the TaskServer class (not inside TaskHandler).

Check that the escaped quotes in the error message match exactly: {\"error\":\"Title is required\"}.

Help me fix my handlePost compilation errors

Test both endpoints with curl

Both endpoints are wired up. Time to prove they work by making real HTTP requests from the terminal.

  • Stop the server if it is still running (Ctrl+C).
  • Recompile and start the server by running:
javac TaskServer.java
java TaskServer
  • In your second terminal, test the GET endpoint by running:
curl http://localhost:8080/tasks

You should see the two seed tasks as JSON:

[{"id":1,"title":"Learn Java HTTP","status":"in-progress"},{"id":2,"title":"Build REST API","status":"todo"}]
  • Create a new task by running this POST request:
curl -X POST -H "Content-Type: application/json" -d "{\"title\":\"Test new task\"}" http://localhost:8080/tasks

You should see the newly created task returned with ID 3 and status todo.

  • Confirm the new task was added by running GET again:
curl http://localhost:8080/tasks

The response should now contain three tasks, including your newly created one.

What just happened?

You just completed a full HTTP request/response cycle for both reading and writing data. The GET request retrieved the current state. The POST request sent new data in the body, and the server parsed it, stored it, and confirmed with a 201 status code.

Seeing an empty response or connection error?

Make sure the server is running in one terminal and you are running curl in a separate terminal. If you see connection refused, the server is not started.

If POST returns an error about the title being required, check that the -d flag includes proper JSON with a title field. The escaped quotes are important on Windows.

Help me debug my curl requests to the /tasks endpoint

✔️ Awesome, I've got everything!

Great. Double check you have saved TaskServer.java with all the changes.

ⓧ I'd like to double check the full code

Here is the complete TaskServer.java at the end of this step:

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.stream.Collectors;

public class TaskServer {

    static ArrayList<Task> tasks = new ArrayList<>();
    static int nextId = 1;

    static class Task {
        int id;
        String title;
        String status;

        Task(int id, String title, String status) {
            this.id = id;
            this.title = title;
            this.status = status;
        }

        String toJson() {
            return "{\"id\":" + id + ",\"title\":\"" + title + "\",\"status\":\"" + status + "\"}";        }
    }

    static String tasksToJson(ArrayList<Task> taskList) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < taskList.size(); i++) {
            sb.append(taskList.get(i).toJson());
            if (i < taskList.size() - 1) {
                sb.append(",");
            }
        }
        sb.append("]");
        return sb.toString();
    }

    static String readRequestBody(HttpExchange exchange) throws IOException {
        InputStream is = exchange.getRequestBody();
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        String body = reader.lines().collect(Collectors.joining());
        reader.close();
        return body;
    }

    static String extractJsonValue(String json, String key) {
        String searchKey = "\"" + key + "\":\"";
        int startIndex = json.indexOf(searchKey);
        if (startIndex == -1) return null;
        startIndex += searchKey.length();
        int endIndex = json.indexOf("\"", startIndex);
        if (endIndex == -1) return null;
        return json.substring(startIndex, endIndex);
    }

    static void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException {
        exchange.getResponseHeaders().set("Content-Type", "application/json");
        byte[] responseBytes = response.getBytes("UTF-8");
        exchange.sendResponseHeaders(statusCode, responseBytes.length);
        OutputStream os = exchange.getResponseBody();
        os.write(responseBytes);
        os.close();
    }

    public static void main(String[] args) throws IOException {
        tasks.add(new Task(nextId++, "Learn Java HTTP", "in-progress"));
        tasks.add(new Task(nextId++, "Build REST API", "todo"));

        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/tasks", new TaskHandler());
        server.setExecutor(null);
        server.start();
        System.out.println("Server started on http://localhost:8080");
        System.out.println("Try: curl http://localhost:8080/tasks");
    }

    static class TaskHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            String method = exchange.getRequestMethod();
            switch (method) {
                case "GET":
                    handleGet(exchange);
                    break;
                case "POST":
                    handlePost(exchange);
                    break;
                default:
                    sendResponse(exchange, 405, "{\"error\":\"Method not allowed\"}");
                    break;
            }
        }

        void handleGet(HttpExchange exchange) throws IOException {
            String response = tasksToJson(tasks);
            sendResponse(exchange, 200, response);
        }

        void handlePost(HttpExchange exchange) throws IOException {
            String body = readRequestBody(exchange);
            String title = extractJsonValue(body, "title");
            if (title == null || title.isEmpty()) {
                sendResponse(exchange, 400, "{\"error\":\"Title is required\"}");
                return;
            }
            Task newTask = new Task(nextId++, title, "todo");
            tasks.add(newTask);
            sendResponse(exchange, 201, newTask.toJson());
        }
    }
}

Your API can now read and create tasks. Next up, you will add DELETE support so clients can remove tasks by ID. This introduces path parameter parsing, where the task ID comes from the URL itself.

Implement DELETE and Method Routing

Your API can list tasks and create new ones. But what happens when a task is finished or no longer relevant?

In this step, you will add a DELETE endpoint so clients can remove tasks by ID. You will also see how your server already rejects unsupported methods with a 405 status code.

In this step, get ready to:
  • Add a DELETE case to the method routing switch statement.
  • Write a handler that extracts a task ID from the URL path and removes the matching task.
  • Test DELETE, 404, and 405 responses using curl.
Update the handler to route DELETE requests

DELETE requests need to know which task to remove. The client sends the task ID as part of the URL path (e.g., /tasks/1). Your handler needs to extract this path before routing the request.

  • In TaskServer.java, find the handle() method inside TaskHandler.
  • Replace the entire handle() method with this updated version:
@Override
public void handle(HttpExchange exchange) throws IOException {
    String method = exchange.getRequestMethod();
    // Extract the full request path (e.g., "/tasks/1")
    String path = exchange.getRequestURI().getPath();

    switch (method) {
        case "GET":
            handleGet(exchange);
            break;
        case "POST":
            handlePost(exchange);
            break;
        case "DELETE":
            // Pass path so the handler can extract the task ID
            handleDelete(exchange, path);
            break;
        default:
            String error = "{\"error\":\"Method not allowed\"}";
            sendResponse(exchange, 405, error);
            break;
    }
}

What does this code do?

The key addition is String path = exchange.getRequestURI().getPath(). This captures the full URL path from the incoming request.

When a client sends DELETE /tasks/1, the path will be /tasks/1. You pass this path into handleDelete() so it can figure out which task to remove.

The default case catches any HTTP method your API does not support (like PATCH or OPTIONS) and returns a 405 Method Not Allowed status code.

Add the DELETE handler method

The DELETE handler needs to do three things: extract the ID from the path, find the matching task, and remove it from the list.

  • Add the following handleDelete() method inside TaskHandler, below the existing handlePost() method:
void handleDelete(HttpExchange exchange, String path) throws IOException {
    // Split path "/tasks/1" into ["", "tasks", "1"]
    String[] parts = path.split("/");
    if (parts.length < 3) {
        sendResponse(exchange, 400, "{\"error\":\"Task ID is required\"}");
        return;
    }

    // Parse the ID segment into an integer
    int id;
    try {
        id = Integer.parseInt(parts[2]);
    } catch (NumberFormatException e) {
        sendResponse(exchange, 400, "{\"error\":\"Invalid task ID\"}");
        return;
    }

    // Remove the task with the matching ID
    boolean removed = tasks.removeIf(task -> task.id == id);
    if (removed) {
        // 204 = success with no response body, -1 means no body
        exchange.sendResponseHeaders(204, -1);
        exchange.getResponseBody().close();
    } else {
        sendResponse(exchange, 404, "{\"error\":\"Task not found\"}");
    }
}

What does this code do?

The path /tasks/1 splits into three parts: an empty string, tasks, and 1. The third element (parts[2]) is the task ID.

The method uses tasks.removeIf() with a lambda to find and delete the matching task in one step. It returns true if a task was removed, false if no task had that ID.

The 204 status code means "success, but there is nothing to send back." Passing -1 to sendResponseHeaders tells the server not to write a response body. If the ID does not match any task, a 404 tells the client the resource was not found.

  • Save TaskServer.java.

Seeing a compile error?

Make sure handleDelete is placed inside the TaskHandler class (after handlePost), not outside it. The method uses tasks from the outer class, so it must be inside TaskHandler.

Also check that the handle() method's switch statement includes the case "DELETE" line calling handleDelete(exchange, path).

Help me fix my handleDelete compile error

Test DELETE and method routing

Time to verify that deletion works, nonexistent IDs return 404, and unsupported methods return 405.

  • Stop any running server by pressing Ctrl+C in the terminal where it is running.
  • Compile and run the updated server:
javac TaskServer.java
java TaskServer
  • In your second terminal, delete the first task by running:
curl -X DELETE http://localhost:8080/tasks/1

You should see no output. A 204 response has no body, which means the deletion succeeded silently.

  • Confirm the task is gone by running:
curl http://localhost:8080/tasks

You should see only the second task in the response. The task with ID 1 is no longer in the list.

  • Test deleting a task that does not exist by running:
curl -X DELETE http://localhost:8080/tasks/99

You should see {"error":"Task not found"} in the response. Your server correctly returns a 404 for missing resources.

  • Test an unsupported HTTP method by running:
curl -X PATCH http://localhost:8080/tasks

You should see {"error":"Method not allowed"} in the response. The default case in your switch statement catches anything your API does not explicitly handle.

Why different status codes for different errors?

Each HTTP status code tells the client exactly what went wrong. A 404 means "this specific resource does not exist." A 405 means "this endpoint exists, but you used the wrong method." A 400 means "your request was malformed."

These distinctions help API clients handle errors programmatically. A frontend app might retry a 404 with a different ID, but show a developer error for a 405.

Still seeing the deleted task?

Make sure you stopped the old server and recompiled with javac TaskServer.java before running java TaskServer again. Java does not hot-reload. You must recompile after every change.

Help me debug my DELETE endpoint

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.stream.Collectors;

public class TaskServer {

    static ArrayList<Task> tasks = new ArrayList<>();
    static int nextId = 1;

    static class Task {
        int id;
        String title;
        String status;

        Task(int id, String title, String status) {
            this.id = id;
            this.title = title;
            this.status = status;
        }

        String toJson() {
            return "{\"id\":" + id + ",\"title\":\"" + title + "\",\"status\":\"" + status + "\"}"; 
        }
    }

    static String tasksToJson(ArrayList<Task> taskList) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < taskList.size(); i++) {
            sb.append(taskList.get(i).toJson());
            if (i < taskList.size() - 1) {
                sb.append(",");
            }
        }
        sb.append("]");
        return sb.toString();
    }

    static String readRequestBody(HttpExchange exchange) throws IOException {
        InputStream is = exchange.getRequestBody();
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        String body = reader.lines().collect(Collectors.joining());
        reader.close();
        return body;
    }

    static String extractJsonValue(String json, String key) {
        String searchKey = "\"" + key + "\":\"";
        int startIndex = json.indexOf(searchKey);
        if (startIndex == -1) return null;
        startIndex += searchKey.length();
        int endIndex = json.indexOf("\"", startIndex);
        if (endIndex == -1) return null;
        return json.substring(startIndex, endIndex);
    }

    static void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException {
        exchange.getResponseHeaders().set("Content-Type", "application/json");
        byte[] responseBytes = response.getBytes("UTF-8");
        exchange.sendResponseHeaders(statusCode, responseBytes.length);
        OutputStream os = exchange.getResponseBody();
        os.write(responseBytes);
        os.close();
    }

    public static void main(String[] args) throws IOException {
        tasks.add(new Task(nextId++, "Learn Java HTTP", "in-progress"));
        tasks.add(new Task(nextId++, "Build REST API", "todo"));

        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/tasks", new TaskHandler());
        server.setExecutor(null);
        server.start();
        System.out.println("Server started on http://localhost:8080");
        System.out.println("Try: curl http://localhost:8080/tasks");
    }

    static class TaskHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            String method = exchange.getRequestMethod();
            String path = exchange.getRequestURI().getPath();

            switch (method) {
                case "GET":
                    handleGet(exchange);
                    break;
                case "POST":
                    handlePost(exchange);
                    break;
                case "DELETE":
                    handleDelete(exchange, path);
                    break;
                default:
                    String error = "{\"error\":\"Method not allowed\"}";
                    sendResponse(exchange, 405, error);
                    break;
            }
        }

        void handleGet(HttpExchange exchange) throws IOException {
            String response = tasksToJson(tasks);
            sendResponse(exchange, 200, response);
        }

        void handlePost(HttpExchange exchange) throws IOException {
            String body = readRequestBody(exchange);
            String title = extractJsonValue(body, "title");
            if (title == null || title.isEmpty()) {
                sendResponse(exchange, 400, "{\"error\":\"Title is required\"}");
                return;
            }
            Task newTask = new Task(nextId++, title, "todo");
            tasks.add(newTask);
            sendResponse(exchange, 201, newTask.toJson());
        }

        void handleDelete(HttpExchange exchange, String path) throws IOException {
            String[] parts = path.split("/");
            if (parts.length < 3) {
                sendResponse(exchange, 400, "{\"error\":\"Task ID is required\"}");
                return;
            }
            int id;
            try {
                id = Integer.parseInt(parts[2]);
            } catch (NumberFormatException e) {
                sendResponse(exchange, 400, "{\"error\":\"Invalid task ID\"}");
                return;
            }
            boolean removed = tasks.removeIf(task -> task.id == id);
            if (removed) {
                exchange.sendResponseHeaders(204, -1);
                exchange.getResponseBody().close();
            } else {
                sendResponse(exchange, 404, "{\"error\":\"Task not found\"}");
            }
        }
    }
}

Your API now supports the full lifecycle: creating, listing, and deleting tasks. Next up, you will put everything together with a final round of testing and wrap up what you have learned about the HTTP request/response cycle.

Secret mission

Add PUT (Update) and Query Parameter Filtering

Your API can create, list, and delete tasks. But what about updating a task's title or marking it as done? And what if you only want to see tasks with a specific status? Time to level up your API with a PUT endpoint and query parameter filtering.

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.
  • The running TaskServer process (if still active in your terminal).
  • JDK 21 installed on your machine.

Keep everything running

No action needed. Choose this if you're still actively building or want to extend your API with more endpoints.

  • Your task-api folder stays on your Desktop.
  • JDK 21 remains installed and available for future Java projects.
  • If the server is still running, you can keep testing with curl commands.

Pause - I'll come back to this later

Stop the running server process to free up port 8080, but keep your project files for later.

  • Switch to the terminal where the server is running and press Ctrl+C to stop it.
  • Your task-api folder and TaskServer.java file remain on your Desktop, ready to pick up where you left off.
  • To restart later, navigate to the folder and run javac TaskServer.java then java TaskServer.

Delete - I don't want to use this again

Remove all project files and free up your machine completely.

  • Stop the server by pressing Ctrl+C in the terminal where it's running.
  • Delete the project folder by running this command on macOS/Linux:
rm -rf ~/Desktop/task-api

On Windows, run:

rmdir /s /q %USERPROFILE%\Desktop\task-api
  • To uninstall JDK 21 on Windows, press the Windows key, search for Add or remove programs, find Java(TM) SE Development Kit 21 in the list, and click Uninstall.
  • On macOS, remove the JDK directory by running:
sudo rm -rf /Library/Java/JavaVirtualMachines/jdk-21.jdk
  • Close any terminal windows you had open for this project.

Nice Work!

Nice Work!

Congratulations! You just built a fully functional REST API from scratch using nothing but Java's standard library. No frameworks, no build tools, no external dependencies.

You've learned how to:

  • Build a running HTTP server using com.sun.net.httpserver.HttpServer with zero external dependencies.
  • Implement REST API routing based on HTTP methods (GET, POST, DELETE) with proper status codes (200, 201, 204, 404, 405).
  • Construct and parse JSON manually to understand the complete HTTP request/response lifecycle of headers, bodies, and status codes.
  • Secret Mission: Added a PUT endpoint for updating existing tasks and query parameter filtering to search tasks by status.

Ready to quiz yourself?