Javalin 7

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")
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:

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:

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:

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:

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:

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:

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:

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.

Like Javalin?
Star us 😊