2 minute read

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 @ProxyConfig or @EnableRouting annotations 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?