Fork me on GitHub

Javalin 6 is finally here! (January 28th, 2024)

Javalin is a Java and Kotlin web framework which focuses on simplicity and Java/Kotlin interoperability. It’s a thin layer on top of the excellent Jetty webserver and focuses primarily on the web layer. Javalin aims to be very lightweight and has a codebase of around 8000 lines of Java/Kotlin code, as well as around 12 000 lines of test (750+ tests).
The project owes much of its success to the remarkably supportive JVM open-source community. Javalin has been around for almost seven years now, and has 183 contributors and 611 forks. 856 pull requests have been merged and 1200 issues have been closed. The project has had five million downloads in the past 12 months.

Okay, let’s have a look at Javalin 6!

Hello World

Javalin’s main goal is simplicity and developer productivity. The “Hello World” example reflects that:

public static void main(String[] args) {
    var app = Javalin.create(/*config*/)
        .get("/", ctx -> ctx.result("Hello World"))
        .start(7070);
}
fun main() {
    val app = Javalin.create(/*config*/)
        .get("/") { ctx -> ctx.result("Hello World") }
        .start(7070)
}

Sending data to clients

The simplest way to send content to a client is through ctx.result("My String"), which sends a text/plain result. Javalin has several options for sending responses:

ctx.result(stringOrStream);           // writes string or input stream to client (`text/plain` by default)
ctx.json(myJson);                     // serializes object to JSON string and writes to client (as `application/json`)
ctx.jsonStream(myJson);               // serializes JSON directly to client (nothing buffered in memory)
ctx.writeSeekableStream(myMediaFile); // stream audio and video to client (supports seeking/skipping)
ctx.future(myFutureSupplier);         // instructs Javalin to handle request asynchronously
ctx.render("/file.ext", model);       // render template or markdown file (as `text/html`)
ctx.result(stringOrStream)            // writes string or input stream to client (`text/plain` by default)
ctx.json(myJson)                      // serializes object to JSON string and writes to client (as `application/json`)
ctx.jsonStream(myJson)                // serializes JSON directly to client (nothing buffered in memory)
ctx.writeSeekableStream(myMediaFile)  // stream audio and video to client (supports seeking/skipping)
ctx.future(myFutureSupplier)          // instructs Javalin to handle request asynchronously
ctx.render("/file.ext", model)        // render template or markdown file (as `text/html`)

As mentioned, Javalin aims to be very lightweight, so it doesn’t have any built-in JSON serialization or templating. Instead, Javalin has JsonMapper and FileRenderer interfaces. There are implementations for Jackson and GSON available, as well as a javalin-rendering artifact, which supports all of the most popular templating engines.

Handling input from clients

Javalin makes it easy to extract and validate client data through dedicated methods:

ctx.body();                     // get the request body as a string (caches the body)
ctx.formParam("name");          // get a form parameter
ctx.queryParam("name");         // get a query parameter
ctx.uploadedFile("name");       // get an uploaded file

// JSON methods
ctx.bodyAsClass(Clazz);         // deserialize ctx.body() to class
ctx.bodyStreamAsClass(Clazz);   // consume input stream from request body and deserialize to class

// validation
var age = ctx.queryParamAsClass("age", Integer.class)  // wraps parameter in Validator
    .check(age -> age > 18, "NOT_OLD_ENOUGH") // adds check with error message
    .get(); // gets the validated value, or throws ValidationException
var bananaBox = ctx.bodyValidator(BananaBox.class)
    .check(box -> box.weight < 5, ValidationError("WEIGHT_TOO_HIGH", Map.of("MAX_WEIGHT", 5)))
    .check(box -> box.bananas.length > 20, ValidationError("NOT_ENOUGH_BANANAS", Map.of("MIN_BANANAS", 20)))
    .getOrDefault(defaultBananaBox) // uses default if body is null, runs validation rules otherwise
ctx.body()                      // get the request body as a string (caches the body)
ctx.formParam("name")           // get a form parameter
ctx.queryParam("name")          // get a query parameter
ctx.uploadedFile("name")        // get an uploaded file

// JSON methods
ctx.bodyAsClass<Clazz>()        // deserialize ctx.body() to class
ctx.bodyStreamAsClass<Clazz>()  // consume input stream from request body and deserialize to class

// validation
val age = ctx.queryParamAsClass<Int>("age")  // wraps parameter in Validator
    .check({ it > 18 }, "NOT_OLD_ENOUGH") // adds check with error message
    .get() // gets the validated value, or throws ValidationException
val bananaBox = ctx.bodyValidator<BananaBox>()
    .check({ it.weight < 5 }, ValidationError("WEIGHT_TOO_HIGH", mapOf("MAX_WEIGHT" to 5)))
    .check({ it.bananas.length > 20 }, ValidationError("NOT_ENOUGH_BANANAS", mapOf("MIN_BANANAS" to 20)))
    .getOrDefault(defaultBananaBox) // uses default if body is null, runs validation rules otherwise

WebSockets and Server-Sent Events

WebSockets and Server-Sent Events are handled with lambdas, similar to most of Javalin’s other APIs:

app.ws("/websocket/{path}", ws -> {
    ws.onConnect(ctx -> System.out.println("Connected"));
    ws.onMessage(ctx -> {
        var user = ctx.messageAsClass(User.class); // convert from json
        ctx.send(user); // convert to json and send back
    });
    ws.onBinaryMessage(ctx -> System.out.println("Message"))
    ws.onClose(ctx -> System.out.println("Closed"));
    ws.onError(ctx -> System.out.println("Errored"));
});

app.sse("/sse", client -> {
    client.sendEvent("connected", "Hello, SSE");  // can also send an object, which will be serialized
    client.onClose(() -> System.out.println("Client disconnected"));
});
app.ws("/websocket/{path}") { ws ->
    ws.onConnect { ctx -> println("Connected") }
    ws.onMessage { ctx ->
        val user = ctx.messageAsClass<User>(); // convert from json
        ctx.send(user); // convert to json and send back
    }
    ws.onBinaryMessage { ctx -> println("Message") }
    ws.onClose { ctx -> println("Closed") }
    ws.onError { ctx -> println("Errored") }
}

app.sse("/sse") { client ->
    client.sendEvent("connected", "Hello, SSE") // can also send an object, which will be serialized
    client.onClose { println("Client disconnected") }
}

Routing

Routing in Javalin can either happen directly on the Javalin instance (usually named app), or through the config.router object. If you’re writing a small/simple application, you can probably get away with just using the app object. If you’re writing a larger application, you might want to use the config.router instead.

// app object
var app = Javalin.create(/*config*/)
    .get("/", ctx -> ctx.result("Hello World"))
    .start(7070);

// config.router object
var app = Javalin.create(config -> {
    config.router.mount(router -> { // access to router instance
        router.get("/", ctx -> ctx.result("Hello World"));
    });
    config.router.apiBuilder(() -> { // sets a static variable scoped to the lambda
        path("users", () -> { // statically imported to improve readability
            get(UserController::getAll);
            post(UserController::create);
            path(":user-id", () -> {
                get(UserController::getOne);
                patch(UserController::update);
                delete(UserController::delete);
            });
        });
    });
}).start(7070);
// app object
val app = Javalin.create(/*config*/)
    .get("/") { ctx -> ctx.result("Hello World") }
    .start(7070)

// config.router object
val app = Javalin.create { config ->
    config.router.mount { router -> // access to router instance
        router.get("/") { ctx -> ctx.result("Hello World") }
    }
    config.router.apiBuilder { // sets a static variable scoped to the lambda
        path("users") { // statically imported to improve readability
            get(UserController::getAll)
            post(UserController::create)
            path(":user-id") {
                get(UserController::getOne)
                patch(UserController::update)
                delete(UserController::delete)
            }
        }
    }
}.start(7070)

Configuring Javalin

To configure Javalin, you can adjust the JavalinConfig using a Consumer in the Javalin#create method. Different configuration options are split into sub-objects, which are accessible through the config object:

var app = Javalin.create(config -> {
    config.http.asyncTimeout = 10_000L;
    config.router.ignoreTrailingSlashes = true;
    config.staticFiles.add("/public", Location.CLASSPATH);
});
val app = Javalin.create { config ->
    config.http.asyncTimeout = 10_000L
    config.router.ignoreTrailingSlashes = true
    config.staticFiles.add("/public", Location.CLASSPATH)
};

For a full list of configuration options, see the configuration section of the docs: /documentation#configuration.

Configuring Jetty

Javalin uses Jetty under the hood, and you can configure Jetty directly through Javalin:

var app = Javalin.create(config -> {
    config.jetty.threadPool = new QueuedThreadPool();
    config.jetty.modifyWebSocketServletFactory(factory -> {});
    config.jetty.modifyServer(server -> {});
    config.jetty.modifyServletContextHandler(handler -> {});
    config.jetty.addConnector((server, httpConfig) -> new ServerConnector(server));
});
val app = Javalin.createAndStart {
    it.jetty.threadPool = QueuedThreadPool()
    it.jetty.modifyWebSocketServletFactory { factory -> }
    it.jetty.modifyServer { server -> }
    it.jetty.modifyServletContextHandler { handler -> }
    it.jetty.addConnector { server, httpConfig -> ServerConnector(server) }
}

For a full list of configuration options, see the configuration section of the docs: /documentation#jettyconfig.

Plugins

Javalin’s plugin system requires plugin author to extend a Plugin abstract class, and provide a Consumer<Config> to the constructor. This ensures that even third party plugins conform to the same API as the core Javalin library:

Javalin.create(config -> {
    config.registerPlugin(new ExamplePlugin(exampleConfig -> {
        exampleConfig.exampleSetting = "example";
    }));
});
Javalin.create { config ->
    config.registerPlugin(ExamplePlugin { exampleConfig ->
        exampleConfig.exampleSetting = "example"
    })
}

For more information about the plugin system, see /plugins/how-to.

OpenAPI plugin

One of the most popular Javalin plugins is its OpenAPI integration:

@OpenApi(
    path = "/api/v1/users",
    methods = [HttpMethod.POST],
    summary = "Register a user",
    tags = ["Users"],
    requestBody = OpenApiRequestBody(
        content = [OpenApiContent(RegistrationRequest::class)],
        required = true,
        description = "Data about the user"
    ),
    responses = [
        OpenApiResponse(status = "200", ...),
        OpenApiResponse(status = "401", ...),
    ]
)
fun register(context: Context) {
    // handler code goes here
}

You can read more about the OpenAPI plugin here: /plugins/openapi.

SslPlugin

Another popular plugin is the SslPlugin, which makes it easy to configure SSL/TLS:

Javalin.create { config ->
    config.registerPlugin(SSLPlugin { ssl ->
        ssl.pemFromPath("/path/to/cert.pem", "/path/to/key.pem")
    })
}.start()

You can read more about the SslPlugin here: /plugins/ssl-helpers.

What’s changed since Javalin 5

Javalin 6 is a major release, and there are quite a few changes, the main ones being:

Please follow the migration guide to upgrade.

Get involved

If you want to contribute to the project, please head over to GitHub or Discord.

If you want to stay up to date, please follow us on Twitter.

Like Javalin?
Star us 😊

×