Shortening a URL in Java is one POST through java.net.http.HttpClient. Build the request, set your API key as a Bearer header, send it, read short_url from the response body. The client has shipped in the JDK since Java 11, so the first working version has no dependencies at all.
Most Java results for this are "build a URL shortener with Spring Boot and JPA", which is the storage problem rather than this one. If that is what you want, how to build a URL shortener covers the design; if you want a short link out of an existing service, start with the free URL shortener API overview for the endpoint shape and auth.
Same walkthrough in the other runtimes: Python, JavaScript, Go, and Ruby.
The Fastest Way: One Request Through a Shared Client
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public final class Shortener {
private static final HttpClient CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
public static void main(String[] args) throws Exception {
String body = """
{"destination_url":"https://example.com/spring-sale?utm_source=newsletter"}""";
HttpRequest request = HttpRequest
.newBuilder(URI.create("https://api.elido.app/v1/links"))
.header("Authorization", "Bearer " + System.getenv("ELIDO_API_KEY"))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
throw new IllegalStateException("shorten failed: " + response.statusCode());
}
System.out.println(response.body()); // {"id":"...","short_url":"https://s.elido.me/ab12cd"}
}
}
Two timeouts, doing different jobs. connectTimeout on the builder bounds how long the TCP and TLS handshake may take; timeout on the request bounds the whole exchange. Set only the first and a server that accepts your connection and then says nothing will hold the thread indefinitely.
The statusCode() >= 400 check is not defensive programming, it is the contract. send throws IOException when the socket dies and HttpTimeoutException when the deadline passes, and never for an HTTP error. A revoked key produces a perfectly ordinary response object with a JSON error in the body.
Parse the body with Jackson in real code. A text block is honest for one hard-coded field and stops being honest the moment a destination URL contains a quote.
Reuse One Client, Not One Per Call
HttpClient is thread-safe and immutable once built, and each instance owns a connection pool plus an executor. Building one inside a helper method means every call re-does the TLS handshake and leaves an executor behind for the garbage collector to notice later. One static instance per application is the whole rule.
Retry on 429 and 5xx Without Creating Duplicates
There is one failure worth designing around. The POST arrives, the link is created, the response is lost on the way back. Your code sees a timeout, retries, and now two short links point at one destination.
An Idempotency-Key fixes it, and deriving that key from the destination rather than a random UUID means a re-run of the same batch is free rather than duplicative.
static String shorten(String destination, int attempts) throws Exception {
String key = HexFormat.of().formatHex(
MessageDigest.getInstance("SHA-256").digest(destination.getBytes(UTF_8)));
for (int attempt = 0; attempt < attempts; attempt++) {
HttpRequest request = HttpRequest
.newBuilder(URI.create("https://api.elido.app/v1/links"))
.header("Authorization", "Bearer " + System.getenv("ELIDO_API_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(
Map.of("destination_url", destination))))
.build();
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
if (status < 400) {
return MAPPER.readTree(response.body()).get("short_url").asText();
}
if (status == 429) {
Thread.sleep(Duration.ofSeconds(response.headers()
.firstValue("Retry-After").map(Long::parseLong).orElse(2L)));
} else if (status >= 500) {
Thread.sleep(Duration.ofSeconds(1L << attempt)); // 1s, 2s, 4s
} else {
throw new IllegalStateException("shorten failed: " + status + " " + response.body());
}
}
throw new IllegalStateException("shorten failed after " + attempts + " attempts");
}
A 401 or 422 exits on the first pass instead of burning three attempts on something that will never change. Only 429 and 5xx are worth waiting on, and the 429 branch honours the server's own number rather than guessing. The reasoning behind all of this is in the rate limits and idempotency deep-dive.
Want to run it against a live endpoint? Create a key on the free plan, export ELIDO_API_KEY, and the code above compiles and runs as written on Java 21.
Bulk: Virtual Threads With a Ceiling
Virtual threads landed for good in Java 21 and they change the shape of this problem. Blocking IO on a virtual thread costs almost nothing, so one thread per URL is no longer reckless. The rate limit, however, has not moved, which is why the Semaphore matters more than the executor.
static Map<String, String> shortenAll(List<String> urls) throws Exception {
Map<String, String> out = new ConcurrentHashMap<>();
Semaphore gate = new Semaphore(8); // 8 requests in flight, whatever urls.size() is
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (String url : urls) {
executor.submit(() -> {
gate.acquire();
try {
out.put(url, shorten(url, 3));
} catch (Exception e) {
out.put(url, "ERROR: " + e.getMessage()); // one bad URL, not a dead batch
} finally {
gate.release();
}
return null;
});
}
} // close() waits for every task to finish
return out;
}
The try-with-resources block is doing real work: closing a virtual-thread executor waits for all submitted tasks, so there is no awaitTermination dance. ConcurrentHashMap keyed by the original URL keeps a partial failure visible and the run repeatable.
On Java 11 to 17 the same shape works with sendAsync and a fixed thread pool. What does not work on any version is CompletableFuture.allOf over an unbounded list: it starts everything at once and collects a pile of 429s.
When a Generated Client Earns Its Place
For one endpoint, the code above is the entire integration and a dependency buys nothing. That flips once you are listing links with pagination, filtering by tag, and mapping half a dozen response shapes: generated models and a client that already knows the retry rules pay for themselves. The API and SDKs page has the current list, and the SDK quickstart shows the typed version of this same call.
Either way the habits are the same: one shared client, a deadline on every request, the status code read before the body, and a stable idempotency key on anything that might run twice. Once links are created from a service rather than a laptop, webhooks for link events beat polling for finding out what happened next.
Read the Cornerstone Series
This sits in the engineering cluster. Start with the free URL shortener API guide, then rate limits and idempotency. The live reference is the API docs, and solutions for developers covers the rest of the surface.
Related on the Blog
Veelgestelde vragen
How do I shorten a URL in Java?
Build a POST with HttpRequest, send it through a shared HttpClient, and read short_url out of the response body. java.net.http has been in the JDK since Java 11, so a working version needs no Maven dependency at all: set the Authorization header, send a small JSON body, and check statusCode() before parsing.
Do I need OkHttp or Apache HttpClient to call a REST API in Java?
Not for this. The built-in java.net.http.HttpClient covers a JSON POST, timeouts, and async sends. A third-party client is worth it when you need interceptors, connection metrics, or HTTP/2 push handling that the JDK client does not expose, which a single create call does not.
Does Java's HttpClient throw on a 404 or 500?
No. It only throws IOException on a transport failure and HttpTimeoutException when the request timeout fires. A 4xx or 5xx comes back as a normal HttpResponse, so you must read statusCode() yourself. Skipping that check is the usual reason a revoked key looks like a successful call.
Should I create a new HttpClient for each request?
No. Each HttpClient carries its own connection pool and executor, so building one per request throws away connection reuse and can pile up threads. Create one static instance for the application and share it; the class is documented as thread-safe and designed for reuse.
How do I shorten many URLs at once in Java?
Run the calls on a virtual-thread executor with a Semaphore capping how many are in flight, usually about eight. Virtual threads make one-thread-per-URL cheap enough to be sane, but the API's rate limit has not changed, so the semaphore is the part that actually keeps the batch alive.
How do I build the JSON body without a library?
For a single field, a text block with the value escaped is fine. Once the body has user-supplied strings or more than two fields, use Jackson or a JSON-B implementation. Hand-built JSON breaks on the first destination URL that contains a quote, and that failure looks like a server error rather than a formatting bug.
Probeer Elido
Plak een URL, krijg een werkende korte link
Geen aanmelding nodig. Link blijft 30 dagen actief. Meld je aan om hem voor altijd te bewaren.
Gratis, geen aanmelding nodig · 2 per dag