Javalin 6 to 7 migration guide

This guide will help you migrate from Javalin 6 to 7. If you find any errors, or if something is missing, please edit this page on GitHub.

Breaking changes

Routing is now configured upfront

Routes are now defined in the config block, ensuring all routes are registered before the server starts. This makes your application configuration more explicit and reliable.

In Javalin 6:

var app = Javalin.create().start();
app.get("/hello", ctx -> ctx.result("Hello World"));
val app = Javalin.create().start()
app.get("/hello") { ctx -> ctx.result("Hello World") }

In Javalin 7:

var app = Javalin.create(config -> {
    config.routes.get("/hello", ctx -> ctx.result("Hello World"));
}).start();
val app = Javalin.create { config ->
    config.routes.get("/hello") { ctx -> ctx.result("Hello World") }
}.start()

You can also use the apiBuilder syntax:

var app = Javalin.create(config -> {
    config.routes.apiBuilder(() -> {
        get("/hello", ctx -> ctx.result("Hello World"));
    });
}).start();
val app = Javalin.create { config ->
    config.routes.apiBuilder {
        get("/hello") { ctx -> ctx.result("Hello World") }
    }
}.start()

Exception and error handlers are now configured upfront

Like routes, exception and error handlers are now configured in the config block.

In Javalin 6:

var app = Javalin.create().start();
app.exception(NullPointerException.class, (e, ctx) -> {
    ctx.status(500).result("Null pointer exception");
});
app.error(404, ctx -> {
    ctx.result("Not found");
});
val app = Javalin.create().start()
app.exception(NullPointerException::class.java) { e, ctx ->
    ctx.status(500).result("Null pointer exception")
}
app.error(404) { ctx ->
    ctx.result("Not found")
}

In Javalin 7:

var app = Javalin.create(config -> {
    config.routes.exception(NullPointerException.class, (e, ctx) -> {
        ctx.status(500).result("Null pointer exception");
    });
    config.routes.error(404, ctx -> {
        ctx.result("Not found");
    });
}).start();
val app = Javalin.create { config ->
    config.routes.exception(NullPointerException::class.java) { e, ctx ->
        ctx.status(500).result("Null pointer exception")
    }
    config.routes.error(404) { ctx ->
        ctx.result("Not found")
    }
}.start()

The same applies to WebSocket exception handlers (app.wsException()config.routes.wsException()).

SSE handlers are now configured upfront

Like routes, SSE (Server-Sent Events) handlers are now configured in the config block.

In Javalin 6:

var app = Javalin.create().start();
app.sse("/sse", client -> {
    client.sendEvent("connected", "Hello, SSE");
});
val app = Javalin.create().start()
app.sse("/sse") { client ->
    client.sendEvent("connected", "Hello, SSE")
}

In Javalin 7:

var app = Javalin.create(config -> {
    config.routes.sse("/sse", client -> {
        client.sendEvent("connected", "Hello, SSE");
    });
}).start();
val app = Javalin.create { config ->
    config.routes.sse("/sse") { client ->
        client.sendEvent("connected", "Hello, SSE")
    }
}.start()

Lifecycle events are now configured upfront

Like routes, lifecycle events are now configured in the config block for consistency and clarity.

In Javalin 6:

var app = Javalin.create();
app.events(event -> {
    event.serverStarting(() -> System.out.println("Server is starting"));
    event.serverStarted(() -> System.out.println("Server is started"));
});
app.start();
val app = Javalin.create()
app.events { event ->
    event.serverStarting { println("Server is starting") }
    event.serverStarted { println("Server is started") }
}
app.start()

In Javalin 7:

var app = Javalin.create(config -> {
    config.events.serverStarting(() -> System.out.println("Server is starting"));
    config.events.serverStarted(() -> System.out.println("Server is started"));
}).start();
val app = Javalin.create { config ->
    config.events.serverStarting { println("Server is starting") }
    config.events.serverStarted { println("Server is started") }
}.start()

The createAndStart() method has been removed

The createAndStart() convenience method has been removed. Use create().start() instead.

In Javalin 6:

var app = Javalin.createAndStart(config -> {
    config.jetty.defaultPort = 8080;
});
val app = Javalin.createAndStart { config ->
    config.jetty.defaultPort = 8080
}

In Javalin 7:

var app = Javalin.create(config -> {
    config.jetty.defaultPort = 8080;
}).start();
val app = Javalin.create { config ->
    config.jetty.defaultPort = 8080
}.start()

ctx.matchedPath() replaced with ctx.endpoint().path()

The Context#matchedPath() method has been replaced with Context#endpoint().

Migration: ctx.matchedPath() becomes ctx.endpoint().path()

In Javalin 6:

app.get("/users/{id}", ctx -> {
    String path = ctx.matchedPath(); // "/users/{id}"
});
app.get("/users/{id}") { ctx ->
    val path = ctx.matchedPath() // "/users/{id}"
}

In Javalin 7:

config.routes.get("/users/{id}", ctx -> {
    String path = ctx.endpoint().path(); // "/users/{id}"
});
config.routes.get("/users/{id}") { ctx ->
    val path = ctx.endpoint().path() // "/users/{id}"
}

The same change applies to WebSocket contexts:

config.routes.ws("/chat/{room}", ws -> {
    ws.onConnect(ctx -> {
        String path = ctx.endpoint().path(); // "/chat/{room}"
    });
});
config.routes.ws("/chat/{room}") { ws ->
    ws.onConnect { ctx ->
        val path = ctx.endpoint().path() // "/chat/{room}"
    }
}

Validation API now returns nullable types

The validation API has been updated to better reflect nullability. Validator methods now return Validator<T?> by default, and you must call .required() to get a non-nullable validator.

Migration: Add .required() before .get() when you expect a non-null value.

In Javalin 6:

Integer age = ctx.queryParamAsClass("age", Integer.class).get();
MyObject body = ctx.bodyValidator(MyObject.class).get();
val age = ctx.queryParamAsClass<Int>("age").get()
val body = ctx.bodyValidator<MyObject>().get()

In Javalin 7:

Integer age = ctx.queryParamAsClass("age", Integer.class).required().get();
MyObject body = ctx.bodyValidator(MyObject.class).required().get();
val age = ctx.queryParamAsClass<Int>("age").required().get()
val body = ctx.bodyValidator<MyObject>().required().get()

If you want to handle nullable values explicitly, you can use .getOrNull():

Integer age = ctx.queryParamAsClass("age", Integer.class).getOrNull(); // Returns null if not present
val age = ctx.queryParamAsClass<Int>("age").getOrNull() // Returns null if not present

This change affects all validation methods:

JavalinVue is now a plugin

JavalinVue configuration has been moved to a bundled plugin for better modularity.

In Javalin 6:

var app = Javalin.create(config -> {
    config.vue.vueAppName = "my-app";
});
val app = Javalin.create { config ->
    config.vue.vueAppName = "my-app"
}

In Javalin 7:

var app = Javalin.create(config -> {
    config.registerPlugin(new JavalinVuePlugin(vue -> {
        vue.vueAppName = "my-app";
    }));
});
val app = Javalin.create { config ->
    config.registerPlugin(JavalinVuePlugin { vue ->
        vue.vueAppName = "my-app"
    })
}

JavalinVue’s LoadableData is disabled by default

The LoadableData JavaScript class is no longer included by default in JavalinVue pages. If you rely on LoadableData, you need to explicitly enable it:

config.registerPlugin(new JavalinVuePlugin(vue -> {
    vue.enableLoadableData = true; // deprecated, consider using Vue Query instead
}));
config.registerPlugin(JavalinVuePlugin { vue ->
    vue.enableLoadableData = true // deprecated, consider using Vue Query instead
})

AliasCheck import has changed

If you use AliasCheck for symlink checking with static files, the import has changed from Jetty’s package to Javalin’s own package:

// Before (Javalin 6)
import org.eclipse.jetty.server.AliasCheck;

// After (Javalin 7)
import io.javalin.http.staticfiles.AliasCheck;

Compression configuration has changed

The compression API has been simplified. Instead of calling methods like brotliAndGzipCompression(), you now set the compressionStrategy property.

In Javalin 6:

var app = Javalin.create(config -> {
    config.http.brotliAndGzipCompression();
});
val app = Javalin.create { config ->
    config.http.brotliAndGzipCompression()
}

In Javalin 7:

import io.javalin.compression.CompressionStrategy;

var app = Javalin.create(config -> {
    config.http.compressionStrategy = CompressionStrategy.GZIP;
});
import io.javalin.compression.CompressionStrategy

val app = Javalin.create { config ->
    config.http.compressionStrategy = CompressionStrategy.GZIP
}

Template rendering is now modular

The javalin-rendering module has been split into separate modules for each template engine.

Migration: Replace javalin-rendering with javalin-rendering-{engine} in your build file, and remove the template engine dependency (it’s now bundled).

Available modules:

Your code doesn’t need to change - the template renderer classes (JavalinVelocity, JavalinFreemarker, etc.) remain in the same package and work the same way.

Pebble was updated from version 3.1.6 to 4.1.0. Depending on your application, you might need to:

Multipart configuration improvements

Multipart configuration is now part of the Jetty config instead of a global singleton, giving you more flexibility. If you were using MultipartUtil.preUploadFunction, configure multipart settings through config.jetty.multipartConfig instead.

Jetty 12 upgrade

If you’re using Jetty-specific APIs directly, note that:

Jetty Session Handler changes

If you’re using custom Jetty session handlers, you’ll need to update your imports and API usage:

Package changes:

API changes:

In Javalin 6:

SessionHandler sessionHandler = new SessionHandler();
SessionCache sessionCache = new DefaultSessionCache(sessionHandler);
sessionCache.setSessionDataStore(factory.getSessionDataStore(sessionHandler.getSessionHandler()));
sessionHandler.setSessionCache(sessionCache);
sessionHandler.setHttpOnly(true);
fun sessionHandler() = SessionHandler().apply {
    sessionCache = DefaultSessionCache(this).apply {
        sessionDataStore = factory.getSessionDataStore(sessionHandler)
    }
    httpOnly = true
}

In Javalin 7:

SessionHandler sessionHandler = new SessionHandler();
SessionCache sessionCache = new DefaultSessionCache(sessionHandler);
sessionCache.setSessionDataStore(factory.getSessionDataStore(sessionHandler));
sessionHandler.setSessionCache(sessionCache);
sessionHandler.setHttpOnly(true);
fun sessionHandler() = SessionHandler().apply {
    sessionCache = DefaultSessionCache(this).also {
        it.sessionDataStore = factory.getSessionDataStore(this)
    }
    isHttpOnly = true
}

Note: In Kotlin, using also instead of apply for the nested DefaultSessionCache block allows you to access both the cache (it) and the outer SessionHandler (this) without needing labeled lambdas or temporary variables

Java 17 required

Javalin 7 requires Java 17 or higher (previously Java 11).

Kotlin standard library dependencies removed

Javalin 7 no longer includes kotlin-stdlib-jdk8 and kotlin-reflect as transitive dependencies. If your project relies on these, you’ll need to add them explicitly to your build file.

JavalinConfig split into JavalinConfig and JavalinState

Javalin’s internal configuration has been split into two classes:

If you were accessing Javalin’s internal config through app.unsafe (previously app.unsafeConfig() in Javalin 6), you now get a JavalinState instance instead of JavalinConfig.

Migration: Update references from app.unsafeConfig().pvt to app.unsafe:

In Javalin 6:

var app = Javalin.create().start();
var jetty = app.unsafeConfig().pvt.jetty;
var jsonMapper = app.unsafeConfig().pvt.jsonMapper;
val app = Javalin.create().start()
val jetty = app.unsafeConfig().pvt.jetty
val jsonMapper = app.unsafeConfig().pvt.jsonMapper

In Javalin 7:

var app = Javalin.create().start();
var jetty = app.unsafe.jetty;
var jsonMapper = app.unsafe.jsonMapper;
val app = Javalin.create().start()
val jetty = app.unsafe.jetty
val jsonMapper = app.unsafe.jsonMapper

Note: Some properties have moved to subconfigs. For example:

Plugin developers: If you’re writing custom plugins, the Plugin.onStart() method now receives JavalinState instead of JavalinConfig:

In Javalin 6:

@Override
public void onStart(JavalinConfig config) {
    config.jetty.addConnector(...);
}
override fun onStart(config: JavalinConfig) {
    config.jetty.addConnector(...)
}

In Javalin 7:

@Override
public void onStart(JavalinState state) {
    state.jetty.addConnector(...);
}
override fun onStart(state: JavalinState) {
    state.jetty.addConnector(...)
}

Other changes

Major dependency updates

Other changes

Additional changes

It’s hard to keep track of everything, but you can look at the full commit log between the last 6.x version and 7.0.0.

If you run into something not covered by this guide, please edit this page on GitHub!

You can also reach out to us on Discord or GitHub.

Like Javalin?
Star us 😊