Building a Tiny Reverse Proxy with Javalin
Building a Tiny Reverse Proxy with Javalin: A No-Nonsense Guide
When setting up a reverse proxy, the “industry standard” often feels like reaching for a sledgehammer to crack a nut. For my latest project, I needed a simple, lightweight bridge to handle routing without the overhead of a massive ecosystem.
While Spring Boot is my usual go-to, its “auto-magic” configuration and large footprint felt unnecessary for a task this focused. Instead, I opted for Javalin, and the results were surprisingly elegant.
Why Javalin over Spring Boot?
If you’re used to the Spring ecosystem, Javalin might look like a toy at first glance. However, for a specialized utility like a reverse proxy, its “micro” nature is a massive feature.
- Small Package Size: A Javalin “fat JAR” is significantly smaller than a Spring Boot equivalent. This means faster deployments and lower memory overhead in Docker containers.
- Zero “Magic”: There are no
@ProxyConfigor@EnableRoutingannotations to debug. You see exactly how the request is handled in a few lines of code. - Performance: Built on the Jetty server, Javalin is designed for high-concurrency and low-latency, which is exactly what a proxy needs to stay “invisible.”
The Architecture
A reverse proxy sits between the client and your backend server. It intercepts the request, forwards it to the “upstream” service, and then pipes the response back to the user.
1. Project Setup
Add the following to your build.gradle (or pom.xml):
dependencies {
implementation("io.javalin:javalin:6.1.3")
implementation("org.slf4j:slf4j-simple:2.0.7")
}
2. The Implementation
The magic happens by capturing the request path and headers, then replaying them against your target server.
import io.javalin.Javalin
import java.net.HttpURLConnection
import java.net.URL
fun main() {
val targetBaseUrl = "https://api.your-internal-service.com"
val app = Javalin.create().start(8080)
// Capture all paths using a wildcard
app.get("/{path<path>}") { ctx ->
val path = ctx.pathParam("path")
val targetUrl = URL("$targetBaseUrl/$path${ctx.queryString()?.let { "?$it" } ?: ""}")
with(targetUrl.openConnection() as HttpURLConnection) {
requestMethod = "GET"
// Forward relevant headers
ctx.headerMap().forEach { (key, value) ->
if (key.lowercase() != "host") setRequestProperty(key, value)
}
// Stream the response back to Javalin's context
ctx.status(responseCode)
inputStream.copyTo(ctx.res().outputStream)
}
}
}
Key Advantages of This Approach
| Feature | Spring Boot / Cloud Gateway | Javalin Proxy |
|---|---|---|
| Startup Time | ~2-5 seconds | ~100-300ms |
| JAR Size | 20MB - 50MB+ | ~1MB - 5MB |
| Configuration | YAML/Annotations | Direct Code (DSL) |
| Learning Curve | Steep | Flat |
Final Thoughts
Javalin proves that you don’t always need a heavy-duty framework to build robust infrastructure tools. By keeping the footprint small, you reduce the attack surface and make the service much easier to maintain.
To learn more about the framework, head over to the Official Javalin Documentation.
Would you like me to show you how to add basic authentication or rate limiting to this proxy setup?