If you're building a Java application that needs to communicate with a REST API, download data from a website, send JSON, or make HTTP requests, you don't necessarily need an external library.
Modern Java includes its own HTTP client in the java.net.http package.
In this tutorial, we'll learn how to use it to make:
- GET requests
- POST requests
- Requests with headers
- JSON requests
- Asynchronous requests
- Requests with query parameters
- Requests with timeouts
- Error handling
By the end, you'll have a reusable pattern for calling APIs directly from Java.
1. What is java.net.http?
The java.net.http package contains Java's built-in HTTP client API.
The three classes you'll use most often are:
HttpClient
HttpRequest
HttpResponse
They represent three different parts of an HTTP operation:
HttpClient
↓
HttpRequest
↓
Server / API
↓
HttpResponse
Think of them this way:
HttpClient = sends requests
HttpRequest = describes what you want to send
HttpResponse = contains what the server sends back
The API supports both HTTP/1.1 and HTTP/2.
2. Your First GET Request
Let's start with the simplest possible example.
Suppose we want to request:
https://jsonplaceholder.typicode.com/posts/1
We first create an HttpClient.
HttpClient client = HttpClient.newHttpClient();
Then create an HttpRequest.
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
.GET()
.build();
Finally, send the request.
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
Here is the complete program:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"https://jsonplaceholder.typicode.com/posts/1"
))
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println(response.body());
}
}
The response should look similar to:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit..."
}
Notice something important:
HttpResponse<String>
We told Java that we want the response body converted into a String.
That happens because we used:
HttpResponse.BodyHandlers.ofString()
3. Reading the HTTP Status Code
Usually, you don't want to blindly trust the body.
You should first check the HTTP status code.
System.out.println(response.statusCode());
For example:
200
means the request was successful.
Common HTTP codes include:
200 OK
201 Created
204 No Content
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
500 Internal Server Error
503 Service Unavailable
You can handle the response like this:
if (response.statusCode() >= 200 &&
response.statusCode() < 300) {
System.out.println(response.body());
} else {
System.out.println(
"Request failed: " + response.statusCode()
);
}
This is a much safer pattern than assuming every HTTP request succeeds.
4. Adding Request Headers
Real APIs often require headers.
For example:
Accept: application/json
Authorization: Bearer TOKEN
User-Agent: MyJavaApplication
You can add a header using:
.header()
Example:
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/api"))
.header("Accept", "application/json")
.header("User-Agent", "MyJavaApp")
.GET()
.build();
You can also use:
.headers()
Example:
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/api"))
.headers(
"Accept", "application/json",
"User-Agent", "MyJavaApp"
)
.GET()
.build();
5. Sending an Authorization Token
Many APIs use bearer-token authentication.
The HTTP header looks like:
Authorization: Bearer YOUR_TOKEN
In Java:
String token = "YOUR_TOKEN";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/api/users"))
.header(
"Authorization",
"Bearer " + token
)
.GET()
.build();
For production applications, avoid hardcoding secret API keys directly into source code.
Instead, use environment variables or another secret-management mechanism.
For example:
String token = System.getenv("API_TOKEN");
6. Sending a POST Request
GET requests retrieve information.
POST requests usually send information.
Suppose we want to send this JSON:
{
"title": "Learning Java HTTP Client",
"body": "java.net.http is surprisingly convenient",
"userId": 1
}
First create the JSON as a string:
String json = """
{
"title": "Learning Java HTTP Client",
"body": "java.net.http is surprisingly convenient",
"userId": 1
}
""";
Then construct the request:
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"https://jsonplaceholder.typicode.com/posts"
))
.header("Content-Type", "application/json")
.POST(
HttpRequest.BodyPublishers.ofString(json)
)
.build();
Send it normally:
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
Complete example:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class PostExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String json = """
{
"title": "Learning Java HTTP Client",
"body": "java.net.http is surprisingly convenient",
"userId": 1
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"https://jsonplaceholder.typicode.com/posts"
))
.header(
"Content-Type",
"application/json"
)
.POST(
HttpRequest.BodyPublishers.ofString(json)
)
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println(
"Status: " + response.statusCode()
);
System.out.println(response.body());
}
}
7. What Are BodyPublishers?
You'll notice this code:
HttpRequest.BodyPublishers.ofString(json)
A BodyPublisher tells Java how to create the body of the HTTP request.
For a string:
HttpRequest.BodyPublishers.ofString("Hello")
For no body:
HttpRequest.BodyPublishers.noBody()
For a file:
HttpRequest.BodyPublishers.ofFile(path)
For raw bytes:
HttpRequest.BodyPublishers.ofByteArray(bytes)
A useful mental model is:
BodyPublisher = data going OUT
8. What Are BodyHandlers?
A BodyHandler tells Java how you want to receive the server's response.
For text:
HttpResponse.BodyHandlers.ofString()
For bytes:
HttpResponse.BodyHandlers.ofByteArray()
To save directly to a file:
HttpResponse.BodyHandlers.ofFile(path)
To ignore the body:
HttpResponse.BodyHandlers.discarding()
A useful mental model is:
BodyPublisher = request body going OUT
BodyHandler = response body coming IN
For example:
HttpResponse<byte[]> response = client.send(
request,
HttpResponse.BodyHandlers.ofByteArray()
);
Now:
response.body()
returns:
byte[]
instead of a String.
9. Downloading a File
The HTTP client can also download files.
For example:
Path destination = Path.of("image.jpg");
HttpResponse<Path> response = client.send(
request,
HttpResponse.BodyHandlers.ofFile(destination)
);
Complete example:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
public class DownloadExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"https://example.com/image.jpg"
))
.GET()
.build();
Path file = Path.of("image.jpg");
HttpResponse<Path> response = client.send(
request,
HttpResponse.BodyHandlers.ofFile(file)
);
System.out.println(
"Downloaded to: " + response.body()
);
}
}
10. Adding a Timeout
Network requests can hang or take longer than expected.
For that reason, you should usually configure timeouts.
You can configure a connection timeout when creating the client:
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
And a request timeout:
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.timeout(Duration.ofSeconds(20))
.GET()
.build();
You will need:
import java.time.Duration;
11. HTTP/2
Java's HTTP client supports HTTP/2.
You can explicitly request it:
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
You can check which version was actually used:
System.out.println(response.version());
Possible results include:
HTTP_1_1
HTTP_2
12. Sending PUT Requests
PUT is commonly used for replacing or updating resources.
Example:
String json = """
{
"name": "John"
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"https://example.com/users/10"
))
.header("Content-Type", "application/json")
.PUT(
HttpRequest.BodyPublishers.ofString(json)
)
.build();
13. Sending DELETE Requests
DELETE is simple:
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"https://example.com/users/10"
))
.DELETE()
.build();
Then send it as usual:
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
14. Sending PATCH Requests
HttpRequest.Builder has convenient methods for:
GET
POST
PUT
DELETE
But there isn't a dedicated .PATCH() method.
Use .method() instead:
String json = """
{
"email": "john@example.com"
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"https://example.com/users/10"
))
.header("Content-Type", "application/json")
.method(
"PATCH",
HttpRequest.BodyPublishers.ofString(json)
)
.build();
15. GET Requests with Query Parameters
Imagine an API expects:
https://example.com/search?q=java&page=2
A simple request is:
String url =
"https://example.com/search?q=java&page=2";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
But values containing spaces or special characters should be URL-encoded.
For example:
String search = URLEncoder.encode(
"Java HTTP Client",
StandardCharsets.UTF_8
);
Then:
String url =
"https://example.com/search?q=" + search;
You will need:
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
16. Synchronous vs Asynchronous Requests
So far, we've been using:
client.send()
This is synchronous.
That means Java waits until the server responds.
send request
↓
wait
↓
receive response
↓
continue program
Example:
HttpResponse<String> response =
client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
Java also provides:
sendAsync()
17. Making an Asynchronous Request
An asynchronous request looks like this:
client.sendAsync(
request,
HttpResponse.BodyHandlers.ofString()
);
Instead of returning:
HttpResponse<String>
it returns:
CompletableFuture<HttpResponse<String>>
Example:
CompletableFuture<HttpResponse<String>> future =
client.sendAsync(
request,
HttpResponse.BodyHandlers.ofString()
);
You can process the response when it becomes available:
future.thenAccept(response -> {
System.out.println(response.statusCode());
System.out.println(response.body());
});
Complete example:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class AsyncExample {
public static void main(String[] args) {
HttpClient client =
HttpClient.newHttpClient();
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(
"https://jsonplaceholder.typicode.com/posts/1"
))
.GET()
.build();
client.sendAsync(
request,
HttpResponse.BodyHandlers.ofString()
).thenAccept(response -> {
System.out.println(
response.statusCode()
);
System.out.println(
response.body()
);
}).join();
}
}
The .join() keeps this small demo program alive until the asynchronous operation finishes.
18. Why Use sendAsync()?
Imagine you need data from three APIs.
With synchronous requests:
Request A ──────── finish
Request B ─────── finish
Request C ───── finish
The total time can roughly become:
A + B + C
With asynchronous requests, they can run concurrently:
Request A ──────────
Request B ──────
Request C ─────────────
Example:
var request1 = HttpRequest.newBuilder()
.uri(URI.create(
"https://jsonplaceholder.typicode.com/posts/1"
))
.GET()
.build();
var request2 = HttpRequest.newBuilder()
.uri(URI.create(
"https://jsonplaceholder.typicode.com/posts/2"
))
.GET()
.build();
var future1 = client.sendAsync(
request1,
HttpResponse.BodyHandlers.ofString()
);
var future2 = client.sendAsync(
request2,
HttpResponse.BodyHandlers.ofString()
);
var response1 = future1.join();
var response2 = future2.join();
System.out.println(response1.body());
System.out.println(response2.body());
19. Reading Response Headers
The HTTP response contains more than just a body.
You can get all headers with:
response.headers()
Example:
System.out.println(
response.headers().map()
);
You can retrieve one specific header:
response.headers()
.firstValue("content-type")
.ifPresent(System.out::println);
firstValue() returns an Optional because the header may not exist.
20. Handling Exceptions Properly
Earlier examples used:
throws Exception
That's convenient for tutorials, but production code should handle exceptions more carefully.
client.send() can throw:
IOException
InterruptedException
A better example:
try {
HttpResponse<String> response =
client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println(response.body());
} catch (IOException e) {
System.err.println(
"Network error: " + e.getMessage()
);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println(
"Request was interrupted"
);
}
Calling:
Thread.currentThread().interrupt();
restores the thread's interrupted status.
21. Network Failure Is Different from HTTP Failure
This distinction is important.
Suppose a server returns:
404 Not Found
Java does not normally throw an exception just because the server returned a 404.
You still receive an:
HttpResponse
and:
response.statusCode()
might return:
404
Exceptions are more likely for things such as:
DNS failure
connection failure
network interruption
timeout
So your application should handle both HTTP failures and network failures.
try {
HttpResponse<String> response =
client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
if (response.statusCode() >= 200 &&
response.statusCode() < 300) {
System.out.println(response.body());
} else {
System.err.println(
"HTTP error: " +
response.statusCode()
);
}
} catch (IOException e) {
System.err.println(
"Network error: " + e.getMessage()
);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
22. Creating a Reusable API Method
Once you start making multiple requests, repeating the same code becomes annoying.
You can create a reusable method:
public static String get(String url)
throws IOException, InterruptedException {
HttpClient client =
HttpClient.newHttpClient();
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
HttpResponse<String> response =
client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
if (response.statusCode() >= 200 &&
response.statusCode() < 300) {
return response.body();
}
throw new IOException(
"HTTP error: " +
response.statusCode()
);
}
Then calling an API becomes:
String result = get(
"https://jsonplaceholder.typicode.com/posts/1"
);
System.out.println(result);
23. Reuse HttpClient
There's one improvement we should make to the previous example.
Instead of creating a new HttpClient for every request, you should generally reuse one.
public class ApiClient {
private final HttpClient client;
public ApiClient() {
this.client = HttpClient.newBuilder()
.connectTimeout(
Duration.ofSeconds(10)
)
.build();
}
public String get(String url)
throws IOException, InterruptedException {
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(
Duration.ofSeconds(20)
)
.GET()
.build();
HttpResponse<String> response =
client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
if (response.statusCode() >= 200 &&
response.statusCode() < 300) {
return response.body();
}
throw new IOException(
"HTTP " +
response.statusCode() +
": " +
response.body()
);
}
}
Usage:
ApiClient api = new ApiClient();
String response = api.get(
"https://jsonplaceholder.typicode.com/posts/1"
);
System.out.println(response);
This is closer to how HTTP code is usually structured in a real application.
24. But What About JSON?
One thing java.net.http does not try to be is a complete JSON library.
Suppose an API returns:
{
"id": 1,
"name": "John"
}
java.net.http can retrieve it:
String json = response.body();
But converting that JSON into:
User user;
is normally handled by another library.
Popular choices include:
- Jackson
- Gson
- JSON-B
A common architecture looks like this:
java.net.http
↓
HTTP communication
↓
JSON String
↓
Jackson / Gson
↓
Java object
For example, with Jackson:
String json = response.body();
User user = objectMapper.readValue(
json,
User.class
);
The HTTP client handles networking.
The JSON library handles serialization and deserialization.
25. A More Realistic REST API Client
Putting everything together:
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class RestClient {
private final HttpClient client;
public RestClient() {
client = HttpClient.newBuilder()
.connectTimeout(
Duration.ofSeconds(10)
)
.build();
}
public String get(String url)
throws IOException, InterruptedException {
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(
Duration.ofSeconds(20)
)
.header(
"Accept",
"application/json"
)
.GET()
.build();
return send(request);
}
public String post(
String url,
String json
) throws IOException, InterruptedException {
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(
Duration.ofSeconds(20)
)
.header(
"Accept",
"application/json"
)
.header(
"Content-Type",
"application/json"
)
.POST(
HttpRequest.BodyPublishers
.ofString(json)
)
.build();
return send(request);
}
private String send(HttpRequest request)
throws IOException, InterruptedException {
HttpResponse<String> response =
client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
int status = response.statusCode();
if (status >= 200 && status < 300) {
return response.body();
}
throw new IOException(
"HTTP " +
status +
": " +
response.body()
);
}
}
Now your application code becomes very simple:
RestClient client = new RestClient();
String result = client.get(
"https://jsonplaceholder.typicode.com/posts/1"
);
System.out.println(result);
Or:
String json = """
{
"title": "Hello",
"body": "Java HTTP Client",
"userId": 1
}
""";
String result = client.post(
"https://jsonplaceholder.typicode.com/posts",
json
);
System.out.println(result);
26. Quick Cheat Sheet
Create a client
HttpClient client =
HttpClient.newHttpClient();
GET
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
POST
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.POST(
HttpRequest.BodyPublishers
.ofString(body)
)
.build();
PUT
.PUT(
HttpRequest.BodyPublishers.ofString(body)
)
DELETE
.DELETE()
PATCH
.method(
"PATCH",
HttpRequest.BodyPublishers.ofString(body)
)
Add a header
.header(
"Content-Type",
"application/json"
)
Send synchronously
HttpResponse<String> response =
client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
Send asynchronously
client.sendAsync(
request,
HttpResponse.BodyHandlers.ofString()
);
Status code
response.statusCode()
Response body
response.body()
Response headers
response.headers()
27. The Mental Model
If you remember nothing else from this tutorial, remember this pattern:
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println(response.body());
Or visually:
1. Create HttpClient
↓
2. Build HttpRequest
↓
3. client.send(...)
↓
4. Receive HttpResponse
↓
5. Check statusCode()
↓
6. Read body()
Once that pattern makes sense, most of the API becomes easy to understand.
Final Thoughts
Java's built-in HTTP client is easy to overlook because developers often immediately reach for external dependencies.
But for many applications, java.net.http gives you everything you need:
- HTTP/1.1
- HTTP/2
- GET
- POST
- PUT
- DELETE
- PATCH
- Headers
- Authentication headers
- Request bodies
- File downloads
- Timeouts
- Synchronous requests
- Asynchronous requests
For simple REST API clients, scripts, desktop applications, microservices, and backend integrations, it can be more than enough.
The main thing it does not replace is a JSON serialization library such as Jackson or Gson.
A useful separation of responsibilities is:
java.net.http
→ talks to the server
Jackson / Gson
→ translates JSON
Your application
→ handles business logic
And that's often all you need to build a clean Java API client.
If you found this useful, the next logical step is combining java.net.http + Jackson to automatically convert API responses into Java records and objects.
About the Author
Deividas Strole is a Full-Stack Developer based in California, specializing in Java, Spring Boot, JavaScript, React, SQL, and AI-powered applications. He writes about software engineering, modern full-stack development, and digital marketing.
Connect with me:
Top comments (0)