Introduction

One of the most important things in programming is making HTTP calls to talk with some API. Java has very good support for this, and in this post, we will see how to call some HTTP endpoints using Java’s HttpClient class. This class is part of the Java 11 standard library, so you don’t need any external dependencies to use it.

A closer look at the HTTP client

Besides the HttpClient class, there are a few other classes that we will use in this post:

  • HttpRequest: This class represents an HTTP request
  • HttpResponse: This class represents an HTTP response
  • HttpRequest.BodyPublishers: This class contains static methods to create request bodies
  • HttpResponse.BodyHandlers: This class contains static methods to create response bodies

The new HTTP APIs can be found in the java.net.http package.

It’s important to note that the new HTTP client is intended to replace the legacy HttpURLConnection class, which is still part of Java but considered outdated. The modern HttpClient not only simplifies making HTTP requests, but also adds powerful features like asynchronous calls, native WebSocket support, and HTTP/2 compatibility—making it a more robust and developer-friendly option overall.

The endpoint we will be using for the following examples is the JSONPlaceholder API, which is a free online REST API that you can use for testing and prototyping.

The steps are similar for all the requests we will be making:

  1. Create an HttpClient instance; can be reused for multiple requests
  2. Create an HttpRequest instance
  3. Send the request and get the response
  4. Process the response, i.e. either return the raw body or parse it into a Java object

Creating an HTTP client

import java.net.http.HttpClient;

try (var client = HttpClient.newHttpClient()) {
    // Use the client to make requests
} catch (IOException | InterruptedException e) {
    throw new RuntimeException(e);
}

Since HttpClient implements java.lang.AutoCloseable we need to close the resource when we are done with it, the easiest way to do this is by wrapping the instantiation with a try-with-resources block.

Creating an HTTP request

GET request

public static final String GET_SINGLE_ENDPOINT = "https://jsonplaceholder.typicode.com/posts/1";

var request = HttpRequest.newBuilder()
        .uri(new URI(GET_SINGLE_ENDPOINT))
        .GET()
        .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

String responseBody = response.body();
int responseStatusCode = response.statusCode();

To fetch all the posts, instead of a single post, we can do the following:

public static final String GET_ALL_ENDPOINT = "https://jsonplaceholder.typicode.com/posts";

var request = HttpRequest.newBuilder().uri(new URI(GET_ALL_ENDPOINT)).GET().build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String responseBody = response.body();

If the raw response body is a JSON string, we can use a library like Jackson or Gson to parse it into a Java object.

import com.fasterxml.jackson.databind.ObjectMapper;

var objectMapper = new ObjectMapper();
Post[] posts = objectMapper.readValue(responseBody, Post[].class);

var gson = new Gson();
Post[] posts = gson.fromJson(responseBody, Post[].class);

Make sure to add the Jackson or Gson dependency to your pom.xml file:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.17.2</version>
</dependency>
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.13.1</version>
</dependency>

For the following code examples, making the request and getting the raw body is the same.

POST request

private static final String POST_ENDPOINT = "https://jsonplaceholder.typicode.com/posts";

var body = """
        {
        "title": "foo",
        "body": "bar",
        "userId": 1
        }
        """;
var request = HttpRequest.newBuilder()
        .uri(new URI(POST_ENDPOINT))
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .headers("Content-Type", "application/json")
        .build();

For a POST with no body, simply use HttpRequest.BodyPublishers.noBody().

PUT request

private static final String PUT_ENDPOINT = "https://jsonplaceholder.typicode.com/posts/1";

var body = """
        {
        "id":1,
        "title": "FOO",
        "body": "BAR",
        "userId": 1
        }
        """;
var request = HttpRequest.newBuilder()
        .uri(URI.create(PUT_ENDPOINT))
        .PUT(HttpRequest.BodyPublishers.ofString(body))
        .headers("Content-Type", "application/json")
        .build();

PATCH request

private static final String PATCH_ENDPOINT = "https://jsonplaceholder.typicode.com/posts/1";

var body = """
        {
        "title" : "dummy title"
        }
        """;
var request = HttpRequest.newBuilder()
        .uri(new URI(PATCH_ENDPOINT))
        .method("PATCH", HttpRequest.BodyPublishers.ofString(body))
        .headers("Content-Type", "application/json")
        .build();

DELETE request

private static final String DELETE_ENDPOINT = "https://jsonplaceholder.typicode.com/posts/1";

var request = HttpRequest.newBuilder()
        .uri(new URI(DELETE_ENDPOINT))
        .DELETE()
        .build();

Making asynchronous calls

Making asynchronous calls is very similar to making synchronous calls. The only difference is that we need to use the sendAsync method instead of the send method, and deal with the CompletableFuture that is returned.

private static void makeAsyncRequest(HttpClient client, HttpRequest request) {
    // send the request asynchronously
    var future = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
            // process the response
            .thenApply(App::handleResponse)
            // consume the response, by printing the first 5 posts
            .thenAccept(posts -> Arrays.stream(posts).limit(5).forEach(System.out::println));
    // wait for the future to complete, before proceeding with the program
    future.join();
}

private static Post[] handleResponse(HttpResponse<String> response) {
    int statusCode = response.statusCode();
    System.out.println("response.statusCode() = " + statusCode);

    if (statusCode == 200) {
        try {
            return new ObjectMapper().readValue(response.body(), Post[].class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    } else {
        System.out.println("Unsuccessful response.");
    }

    return new Post[]{};
}

Specifying other options

When building the client, we can specify various options, such as:

  • the proxy to use: .proxy(ProxySelector.getDefault())
  • the redirect policy: .followRedirects(HttpClient.Redirect.ALWAYS)
  • the authenticator: .authenticator(auth)
  • the executor: .executor(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()))

When building the request, we can specify various options, such as:

  • the timeout: .timeout(Duration.ofSeconds(1))
  • the headers: .header("Content-Type", "application/json")
  • the protocol version: .version(HttpClient.Version.HTTP_2)
  • the body publishers: .POST(HttpRequest.BodyPublishers.ofString(body))

There are a couple of other body publishers that we can use, such as:

  • HttpRequest.BodyPublishers.ofFile(): body content taken from the file
  • HttpRequest.BodyPublishers.ofInputStream(): body content taken from an input stream
  • HttpRequest.BodyPublishers.ofByteArray(): body content taken from the byte array

When sending the request we can specify how the response body should be handled, such as:

  • HttpResponse.BodyHandlers.ofString(): returns the response body as a string
  • HttpResponse.BodyHandlers.ofByteArray(): returns the response body as a byte array
  • HttpResponse.BodyHandlers.ofFile(): returns the response body as a file
  • HttpResponse.BodyHandlers.ofInputStream(): returns the response body as an input stream
  • HttpResponse.BodyHandlers.ofPublisher(): returns the response body as a publisher
  • HttpResponse.BodyHandlers.ofLines(): returns the response body as a stream of lines

Authentication

When creating the client, we can specify an Authenticator to use for authentication. The Authenticator is a class that provides a way to authenticate requests. We can extend it, and override the getPasswordAuthentication method to provide the credentials.

import java.net.Authenticator;
import java.net.PasswordAuthentication;

public class MyAuthenticator extends Authenticator {

    @Override
    protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication(
                "username",
                "password".toCharArray()
        );
    }
}

Alternatively, we can set the Authorization header in the request, and use a basic authentication scheme or a bearer token.

// for the basic authentication scheme
var auth = Base64.getEncoder().encodeToString("username:password".getBytes());

var getRequest = HttpRequest.newBuilder(new URI("<endpoint>"))
        // set a bearer token
        .header("Authorization", "Bearer <token>")
        // or set a basic authentication scheme
        .header("Authorization", "Basic " + auth)
        .GET()
        .build();

Conclusion

In this post, we have seen how to make HTTP calls using Java’s HttpClient class. We have seen how to create a client, create a request, and send the request. We have also seen how to make asynchronous calls and specify various options for the client and the request.