Important: This news article covers an old version of Javalin (v5.0.0). The current version is v6.3.0.
See the documentation page for up-to-date information.

Foreword

Javalin is a Java and Kotlin web framework which focuses on simplicity and Java/Kotlin interoperability. It supports WebSockets and HTTP3, and it uses Virtual Threads (from Project Loom) by default. Javalin aims to be very lightweight and has a codebase of around 7000 lines of Java/Kotlin code, as well as around 10 000 lines of test (629 tests).
The project would not have been possible without the amazingly supportive JVM open-source community. Javalin has been around for five years now and has 161 contributors and 497 forks. 552 pull requests have been merged and 990 issues have been closed. The project has had three million downloads in the past 12 months.

I’d like to extend my special thanks to one of our newer contributors, @dzikoysk, who has been very helpful in getting v5 ready. Thank you, your contributions have given me a lot of motivation!

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

Hello Javalin 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(/*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:

  • Java
  • Kotlin
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`)

Handling input from clients

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

  • Java
  • Kotlin
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:

  • Java
  • Kotlin
app.ws("/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"));
});
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 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
}).start(port);
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
}.start(port)

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)
Config#accessManager        // can be configured to run before endpoints (get/post/patch/etc)
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)
Config#requestLogger        // runs after response is written to client

Configuring Javalin

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

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

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 -> {
    config.jetty.server(() -> Server());                 // set the Jetty Server
    config.jetty.sessionHandler(() -> SessionHandler()); // set the Jetty SessionHandler
    config.jetty.contextHandlerConfig(handler -> {});    // configure the Jetty ServletContextHandler
    config.jetty.wsFactoryConfig((factory) -> {});       // configure the Jetty WebSocketServletFactory
});
val app = Javalin.create { config ->
    config.jetty.server { Server() }                 // set the Jetty Server
    config.jetty.sessionHandler { SessionHandler() } // set the Jetty SessionHandler
    config.jetty.contextHandlerConfig { handler -> } // configure the Jetty ServletContextHandler
    config.jetty.wsFactoryConfig { factory -> }      // configure the Jetty WebSocketServletFactory
}

Session handling is a particularly useful Jetty feature, as can be seen in /tutorials/jetty-session-handling.

Plugins

There are many third-party open-source plugins available for Javalin, and as of Javalin 5 we’re launching a plugin “marketplace” on javalin.io/plugins.

OpenAPI support

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
}

What’s changed since Javalin 4

The biggest change is that Javalin no longer works with Java 8. We have moved to Jetty 11, which requires Java 11. We’ve also restructured a bit, reworked configuration and futures, split out some of the modules into separate Maven artifacts, and fixed one or two bugs.

You can read more in the migration guide: /migration-guide-javalin-4-to-5.

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.