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.