
Introducing Javalin 7 (February 22, 2026)
Javalin is a Java and Kotlin web framework focused on simplicity and developer productivity. It’s a thin programmatic layer on top of Jetty, meaning no annotations, no magic, no unnecessary abstraction, just straightforward HTTP.
Javalin 7 requires Java 17 and Jetty 12, and brings an improved configuration model, a more consistent plugin API, and a cleaner overall architecture. It’s the result of nearly nine years of community feedback, with over 2 million monthly downloads, 8.2k GitHub stars, and contributions from 202 developers around the world.
Hello World
Add the dependency, then write your first Javalin app in Java or Kotlin:
implementation("io.javalin:javalin:7.0.0")
- Java
- Kotlin
void main() {
var app = Javalin.create(config -> {
config.routes.get("/", ctx -> ctx.result("Hello World"));
}).start(7070);
}
fun main() {
val app = Javalin.create { config ->
config.routes.get("/") { ctx -> ctx.result("Hello World") }
}.start(7070)
}
Building REST APIs with Javalin
Creating an application with Javalin is very straightforward. Here’s a complete server with an API, static files, and WebSockets:
- Java
- Kotlin
var app = Javalin.start(config -> {
config.jetty.port = 7070;
config.staticFiles.add("/public", Location.CLASSPATH);
config.routes.apiBuilder(() -> {
path("users", () -> {
get(UserController::getAll);
post(UserController::create);
path("{user-id}", () -> {
get(UserController::getOne);
patch(UserController::update);
delete(UserController::delete);
});
});
ws("/events", ws -> {
ws.onMessage(ctx -> ctx.send(ctx.message()));
});
});
});
val app = Javalin.start { config ->
config.jetty.port = 7070
config.staticFiles.add("/public", Location.CLASSPATH)
config.routes.apiBuilder {
path("users") {
get(UserController::getAll)
post(UserController::create)
path("{user-id}") {
get(UserController::getOne)
patch(UserController::update)
delete(UserController::delete)
}
}
ws("/events") { ws ->
ws.onMessage { ctx -> ctx.send(ctx.message()) }
}
}
}
Sending data to clients
Javalin 7 offers many convenient methods 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 also makes it easy to extract and validate client data:
- 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
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
WebSockets and Server-Sent Events
WebSockets and Server-Sent Events are also easy to set up:
- Java
- Kotlin
config.routes.ws("/websocket/{path}", ws -> {
ws.onConnect(ctx -> System.out.println("Connected"));
ws.onMessage(ctx -> {
var user = ctx.messageAsClass(User.class);
ctx.send(user);
});
ws.onClose(ctx -> System.out.println("Closed"));
});
config.routes.sse("/sse", client -> {
client.sendEvent("connected", "Hello, SSE");
client.onClose(() -> System.out.println("Client disconnected"));
});
config.routes.ws("/websocket/{path}") { ws ->
ws.onConnect { ctx -> println("Connected") }
ws.onMessage { ctx ->
val user = ctx.messageAsClass<User>()
ctx.send(user)
}
ws.onClose { ctx -> println("Closed") }
}
config.routes.sse("/sse") { client ->
client.sendEvent("connected", "Hello, SSE")
client.onClose { println("Client disconnected") }
}
Configuring Javalin
Javalin 7 makes configuration explicit and organized. Everything is configured upfront in the create() block:
- Java
- Kotlin
var app = Javalin.create(config -> {
// HTTP configuration
config.http.asyncTimeout = 10_000L;
config.http.generateEtags = true;
// Router configuration
config.router.ignoreTrailingSlashes = true;
config.router.caseInsensitiveRoutes = true;
// Static files
config.staticFiles.add("/public", Location.CLASSPATH);
// Jetty configuration
config.jetty.port = 8080;
}).start();
val app = Javalin.create { config ->
// HTTP configuration
config.http.asyncTimeout = 10_000L
config.http.generateEtags = true
// Router configuration
config.router.ignoreTrailingSlashes = true
config.router.caseInsensitiveRoutes = true
// Static files
config.staticFiles.add("/public", Location.CLASSPATH)
// Jetty configuration
config.jetty.port = 8080
}.start()
For a full list of configuration options, see the documentation.
Plugins
Javalin’s plugin system enforces a consistent, consumer-based API, the same pattern used throughout the rest of Javalin’s configuration.
To create a plugin, extend Plugin<CONFIG> and override onStart:
- Java
- Kotlin
class ExamplePlugin extends Plugin<ExamplePlugin.Config> {
ExamplePlugin(Consumer<Config> userConfig) { super(userConfig, new Config()); }
@Override
public void onStart(JavalinState state) {
state.routes.get("/example", ctx -> ctx.result(pluginConfig.message));
}
public static class Config {
public String message = "Hello, plugin!";
}
}
class ExamplePlugin(userConfig: Consumer<Config>? = null) : Plugin<ExamplePlugin.Config>(userConfig, Config()) {
override fun onStart(state: JavalinState) {
state.routes.get("/example") { ctx -> ctx.result(pluginConfig.message) }
}
class Config { var message = "Hello, plugin!" }
}
Plugins are registered via config.registerPlugin, and users can configure them inline:
- Java
- Kotlin
Javalin.create(config -> {
config.registerPlugin(new ExamplePlugin(c -> c.message = "Hi!"));
});
Javalin.create { config ->
config.registerPlugin(ExamplePlugin { c -> c.message = "Hi!" })
}
For more information about the plugin system, see /plugins/how-to.
Upgrading from Javalin 6
Javalin 7 is a major release built on Jetty 12, with improved configuration and modularity. If you’re upgrading from Javalin 6, please follow the migration guide for detailed instructions.
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.