Javalin is a very lightweight Java and Kotlin web framework which focuses on simplicity and Java/Kotlin interoperability. The codebase is about 7000 lines of mixed Java and Kotlin code, and about 10 000 lines of test.
The project would not have been possible without JVM open-source community, which is one of the things that help make the JVM ecosystem the best in the world. Javalin has been around for four years now, and at the time of writing has 135 contributors, 431 forks, and about 130 000 monthly downloads. 530 pull requests have been merged and 828 issues have been closed since 2017. Okay, enough fluff, let’s get to it!

Hello World

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

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

Setting different results

The simplest way to send content to a client is through ctx.result("String"), which just sends a text/plain result. Javalin has support for many other result types:

ctx.json(myJson));                // serializes object to JSON string and writes to client
ctx.jsonStream(myJson));          // serializes JSON directly to client (nothing buffered in memory)
ctx.seekableStream(myMediaFile)); // stream audio and video to client (supports seeking/skipping)
ctx.future(myFuture));            // let's Javalin handle request asynchronously (supports callbacks)
ctx.render("/file.ext", model));  // render template or markdown file

Handling client data

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

// basic 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 inputstream from request and deserialize to class

// validation
Integer 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
BananaBox 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

WebSockets and Server-Sent Events

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

  • Java
  • Kotlin"/websocket/{path}", ws -> {
    ws.onConnect(ctx -> System.out.println("Connected"));
    ws.onMessage(ctx -> {
        User 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"));
});"/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 and request lifecycle

Routing in Javalin can either happen directly on the Javalin instance (usually named app), or through a set of util-methods which improves readability. Please note that these util-method do not hold any global state, but function as normal util-methods (Util.method(app, ...)) with a fancy syntax.

  • Java
  • Kotlin
import static io.javalin.apibuilder.ApiBuilder.*
app.routes(() -> {
    before(GlobalController::globalAction)               // handler that runs for every request to the app
    path("users", () -> {                                // push subpath /users on the router
        get(UserController::getAll);                     // get controller for /users/
        post(UserController::create);                    // post controller for /users/
        before("{userId}*", UserController:userIdCheck); // handler that runs for every request to /users/{userId} as well as al subpaths
        path("{userId}", (() -> {                        // new subpath /{userId} on the router
            get(UserController::getOne);                 // get controller for /users/{userId}
            patch(UserController::update);               // patch controller for /users/{userId}
            path("subpath", (() -> { ... });             // push subpath /subpath on the router (and pop it immediately)
        });                                              // pop subpath /{userId} on the router
        ws("events", UserController::webSocketEvents);   // websocket controller for /users/events
    });                                                  // pop subpath /users on the router
import static io.javalin.apibuilder.ApiBuilder.*
app.routes {
    before(GlobalController::globalAction)               // handler that runs for every request to the app
    path("users") {                                      // push subpath /users on the router
        get(UserController::getAll)                      // get controller for /users/
        post(UserController::create)                     // post controller for /users/
        before("{userId}*", UserController:userIdCheck)  // handler that runs for every request to /users/{userId} as well as al subpaths
        path("{userId}") {                               // new subpath /{userId} on the router
            get(UserController::getOne)                  // get controller for /users/{userId}
            patch(UserController::update)                // patch controller for /users/{userId}
            path("subpath") { ... }                      // push subpath /subpath on the router (and pop it immediately)
        }                                                // pop subpath /{userId} on the router
        ws("events", UserController::webSocketEvents)    // websocket controller for /users/events
    }                                                    // pop subpath /users on the router

Request lifecycle

The Javalin request lifecycle is pretty straightforward. The following snippet covers every place you can hook into:

Javalin#before              // runs first, can throw exception (which will skip any endpoint handlers)
Javalin#get/post/patch/etc  // runs second, can throw exception
Javalin#after               // runs third, can throw exception
Javalin#error               // runs fourth, can throw exception
Javalin#exception           // runs any time a handler throws (cannot throw exception)
JavalinConfig#requestLogger // runs after response is written to client
JavalinConfig#accessManager // wraps all your endpoint handlers in a lambda of your choice

Configuring the server

To configure Javalin, you can adjust the JavalinConfig using a Consumer in the Javalin#create method:

  • Java
  • Kotlin
var app = Javalin.create(config -> {
    config.autogenerateEtags = true;
    config.addStaticFiles("/public", Location.CLASSPATH);
    config.asyncRequestTimeout = 10_000L;
    config.enforceSsl = true;
val app = Javalin.create { config ->
    config.autogenerateEtags = true
    config.addStaticFiles("/public", Location.CLASSPATH);
    config.asyncRequestTimeout = 10_000L
    config.enforceSsl = true

Configuring Jetty

Javalin is built on top of Jetty, and unlike many other web frameworks it doesn’t try to make this a loose coupling. This gives you access to many nice features that are only available in Jetty:

  • Java
  • Kotlin
var app = Javalin.create(config -> {
    wsFactoryConfig((factory) -> {})                // configure the Jetty WebSocketServletFactory
    server(() -> Server())                          // set the Jetty Server
    sessionHandler(() -> SessionHandler())          // set the Jetty SessionHandler
    configureServletContextHandler(handler -> {})   // configure the Jetty ServletContextHandler
val app = Javalin.create { config ->
    wsFactoryConfig { factory -> }                  // configure the Jetty WebSocketServletFactory
    server { Server() }                             // set the Jetty Server
    sessionHandler { SessionHandler() }             // set the Jetty SessionHandler
    configureServletContextHandler { handler -> }   // configure the Jetty ServletContextHandler

Session handling is a particularly useful Jetty feature /tutorials/jetty-session-handling.


There are many third-party open-source plugins available for Javalin, and a few of them have made it into the core:

  • OpenAPI - Create OpenAPI specs for your endpoints and host documentation with either Swagger or ReDoc (/plugins/openapi)
  • GraphQL - Create a GraphQL server in a couple of lines (/plugins/graphql)
  • Vue - Create super simple Vue frontends without the need for WebPack, Vite, etc (/plugins/javalinvue)

What’s changed since Javalin 3

A full overview can be found in the Javalin 3 to 4 migration guide, but you can see some of the highlights below:

  • All Javalin modules now work with JDK 16 and Kotlin 1.5
  • The default Server uses a Loom based ThreadPool if you are using a JDK with Loom.
  • Routing has been extended and now uses {param} instead of :param syntax. This allows for much more complex routing. There is also a <param> syntax for supporting slashes in path parameters.
  • The Validator class has been completely reworked, it now has much better support for customization.
  • A new module for testing, javalin-testtools, has been added.
  • The reified methods on Context that were causing issue with mocking libraries (Mockito and Mockk) have been renamed. This solves tests behaving in nondeterministic ways.
  • Static files have been completely reworked. All global settings related to static files have been removed, and more advanced options have been added to each static file handler.
  • Future handling has been reworked, you can now attach your own callback
  • JSON-mapping is now configured per instance, instead of through a singleton

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.