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:
- 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`)
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:
- 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 -> {
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.
- Java
- Kotlin
// 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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- Java
- Kotlin
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:
- The
AccessManager
has been removed - The plugin system has been completely rewritten
- Jetty configuration has been reworked
- Virtual Threads are now opt-in
- Untyped app-attributes have been replaced with typed app-data
- … and much more!
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.