Fork me on GitHub

Documentation

The documentation is for the latest version of Javalin, currently 6.1.3. Javalin follows semantic versioning, meaning there are no breaking changes unless the major (leftmost) digit changes, for example 5.X.X to 6.X.X.

If you want to support Javalin, consider sponsoring or starring: Support Javalin?

Getting Started

Add the dependency:

<dependency>
    <groupId>io.javalin</groupId>
    <artifactId>javalin</artifactId>
    <version>6.1.3</version>
</dependency>

Not familiar with Maven? Read our Maven tutorial.

implementation("io.javalin:javalin:6.1.3")

Not familiar with Gradle? Read our Gradle tutorial.

libraryDependencies += "io.javalin" % "javalin" % "6.1.3"
@Grab(group='io.javalin', module='javalin', version='6.1.3')
[io.javalin/javalin "6.1.3"]
'io.javalin:javalin:jar:6.1.3'
<dependency org="io.javalin" name="javalin" rev="6.1.3" />

If you want Javalin with testing tools, Jackson and Logback, you can use the artifact id javalin-bundle instead of javalin.

Start coding:

import io.javalin.Javalin;

public class HelloWorld {
    public static void main(String[] args) {
        var app = Javalin.create(/*config*/)
            .get("/", ctx -> ctx.result("Hello World"))
            .start(7070);
    }
}
import io.javalin.Javalin

fun main() {
    val app = Javalin.create(/*config*/)
        .get("/") { ctx -> ctx.result("Hello World") }
        .start(7070)
}

Handlers

Javalin has three main handler types: before-handlers, endpoint-handlers, and after-handlers. (There are also exception-handlers and error-handlers, but we’ll get to them later). The before-, endpoint- and after-handlers require three parts:

The Handler interface has a void return type. You use a method like ctx.result(result), ctx.json(obj), or ctx.future(future) to set the response which will be returned to the user.

If you add multiple before/after handlers for the same path, they will be executed in the order they were added. This can be useful for adding authentication, caching, logging, etc.

You can learn about how Javalin handles concurrency in FAQ - Concurrency.

Before handlers

Before-handlers are matched before every request (including static files).

You might know before-handlers as filters, interceptors, or middleware from other libraries.
app.before(ctx -> {
    // runs before all requests
});
app.before("/path/*", ctx -> {
    // runs before request to /path/*
});
app.before { ctx ->
    // runs before all requests
}
app.before("/path/*") { ctx ->
    // runs before request to /path/*
}

In some cases, you might want to only run a before-handler if the request will be matched (not 404). In this case you can use the app.beforeMatched method:

app.beforeMatched(ctx -> {
    // runs before all matched requests (including static files)
});
app.beforeMatched { ctx ->
    // runs before all matched requests (including static files)
}

Endpoint handlers

Endpoint handlers are the main handler type, and defines your API. You can add a GET handler to serve data to a client, or a POST handler to receive some data. Common methods are supported directly on the Javalin class (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS), uncommon operations (TRACE, CONNECT) are supported via Javalin#addHandler.

Endpoint-handlers are matched in the order they are defined.

You might know endpoint-handlers as routes or middleware from other libraries.
app.get("/output", ctx -> {
    // some code
    ctx.json(object);
});

app.post("/input", ctx -> {
    // some code
    ctx.status(201);
});
app.get("/output") { ctx ->
    // some code
    ctx.json(object)
}

app.post("/input") { ctx ->
    // some code
    ctx.status(201)
}

Handler paths can include path-parameters. These are available via ctx.pathParam("key"):

app.get("/hello/{name}", ctx -> { // the {} syntax does not allow slashes ('/') as part of the parameter
    ctx.result("Hello: " + ctx.pathParam("name"));
});
app.get("/hello/<name>", ctx -> { // the <> syntax allows slashes ('/') as part of the parameter
    ctx.result("Hello: " + ctx.pathParam("name"));
});
app.get("/hello/{name}") { ctx -> // the {} syntax does not allow slashes ('/') as part of the parameter
    ctx.result("Hello: " + ctx.pathParam("name"))
}
app.get("/hello/<name>") { ctx -> // the <> syntax allows slashes ('/') as part of the parameter
    ctx.result("Hello: " + ctx.pathParam("name"))
}

Handler paths can also include wildcard parameters:

app.get("/path/*", ctx -> { // will match anything starting with /path/
    ctx.result("You are here because " + ctx.path() + " matches " + ctx.matchedPath());
});
app.get("/path/*") { ctx -> // will match anything starting with /path/
    ctx.result("You are here because " + ctx.path() + " matches " + ctx.matchedPath())
}

However, you cannot extract the value of a wildcard. Use a slash accepting path-parameter (<param-name> instead of {param-name}) if you need this behavior.

After handlers

After-handlers run after every request (even if an exception occurred)

You might know after-handlers as filters, interceptors, or middleware from other libraries.
app.after(ctx -> {
    // run after all requests
});
app.after("/path/*", ctx -> {
    // runs after request to /path/*
});
app.after { ctx ->
    // run after all requests
}
app.after("/path/*") { ctx ->
    // runs after request to /path/*
}

In some cases, you might want to only run an after-handler if the request will be matched (not 404). In this case you can use the app.afterMatched method:

app.afterMatched(ctx -> {
    // runs after all matched requests (including static files)
});
app.afterMatched { ctx ->
    // runs after all matched requests (including static files)
}

Context

The Context object provides you with everything you need to handle a http-request. It contains the underlying servlet-request and servlet-response, and a bunch of getters and setters.

// Request methods
body()                                // request body as string
bodyAsBytes()                         // request body as array of bytes
bodyAsClass(clazz)                    // request body as specified class (deserialized from JSON)
bodyStreamAsClass(clazz)              // request body as specified class (memory optimized version of above)
bodyValidator(clazz)                  // request body as validator typed as specified class
bodyInputStream()                     // the underyling input stream of the request
uploadedFile("name")                  // uploaded file by name
uploadedFiles("name")                 // all uploaded files by name
uploadedFiles()                       // all uploaded files as list
uploadedFileMap()                     // all uploaded files as a "names by files" map
formParam("name")                     // form parameter by name, as string
formParamAsClass("name", clazz)       // form parameter by name, as validator typed as specified class
formParams("name")                    // list of form parameters by name
formParamMap()                        // map of all form parameters
pathParam("name")                     // path parameter by name as string
pathParamAsClass("name", clazz)       // path parameter as validator typed as specified class
pathParamMap()                        // map of all path parameters
basicAuthCredentials()                // basic auth credentials (or null if not set)
attribute("name", value)              // set an attribute on the request
attribute("name")                     // get an attribute on the request
attributeOrCompute("name", ctx -> {}) // get an attribute or compute it based on the context if absent
attributeMap()                        // map of all attributes on the request
contentLength()                       // content length of the request body
contentType()                         // request content type
cookie("name")                        // request cookie by name
cookieMap()                           // map of all request cookies
header("name")                        // request header by name (can be used with Header.HEADERNAME)
headerAsClass("name", clazz)          // request header by name, as validator typed as specified class
headerMap()                           // map of all request headers
host()                                // host as string
ip()                                  // ip as string
isMultipart()                         // true if the request is multipart
isMultipartFormData()                 // true if the request is multipart/formdata
method()                              // request methods (GET, POST, etc)
path()                                // request path
port()                                // request port
protocol()                            // request protocol
queryParam("name")                    // query param by name as string
queryParamAsClass("name", clazz)      // query param parameter by name, as validator typed as specified class
queryParams("name")                   // list of query parameters by name
queryParamMap()                       // map of all query parameters
queryString()                         // full query string
scheme()                              // request scheme
sessionAttribute("name", value)       // set a session attribute
sessionAttribute("name")              // get a session attribute
consumeSessionAttribute("name")       // get a session attribute, and set value to null
cachedSessionAttribute("name", value) // set a session attribute, and cache the value as a request attribute
cachedSessionAttribute("name")        // get a session attribute, and cache the value as a request attribute
cachedSessionAttributeOrCompute(...)  // same as above, but compute and set if value is absent
sessionAttributeMap()                 // map of all session attributes
url()                                 // request url
fullUrl()                             // request url + query string
contextPath()                         // request context path
userAgent()                           // request user agent
req()                                 // get the underlying HttpServletRequest

// Response methods
result("result")                      // set result stream to specified string (overwrites any previously set result)
result(byteArray)                     // set result stream to specified byte array (overwrites any previously set result)
result(inputStream)                   // set result stream to specified input stream (overwrites any previously set result)
future(futureSupplier)                // set the result to be a future, see async section (overwrites any previously set result)
writeSeekableStream(inputStream)      // write content immediately as seekable stream (useful for audio and video)
result()                              // get current result stream as string (if possible), and reset result stream
resultInputStream()                   // get current result stream
contentType("type")                   // set the response content type
header("name", "value")               // set response header by name (can be used with Header.HEADERNAME)
redirect("/path", code)               // redirect to the given path with the given status code
status(code)                          // set the response status code
status()                              // get the response status code
cookie("name", "value", maxAge)       // set response cookie by name, with value and max-age (optional).
cookie(cookie)                        // set cookie using javalin Cookie class
removeCookie("name", "/path")         // removes cookie by name and path (optional)
json(obj)                             // calls result(jsonString), and also sets content type to json
jsonStream(obj)                       // calls result(jsonStream), and also sets content type to json
html("html")                          // calls result(string), and also sets content type to html
render("/template.tmpl", model)       // calls html(renderedTemplate)
res()                                 // get the underlying HttpServletResponse

// Other methods
async(runnable)                       // lifts request out of Jetty's ThreadPool, and moves it to Javalin's AsyncThreadPool
async(asyncConfig, runnable)          // same as above, but with additonal config
handlerType()                         // handler type of the current handler (BEFORE, AFTER, GET, etc)
appData(typedKey)                     // get data from the Javalin instance (see app data section below)
with(pluginClass)                     // get context plugin by class, see plugin section below
matchedPath()                         // get the path that was used to match this request (ex, "/hello/{name}")
endpointHandlerPath()                 // get the path of the endpoint handler that was used to match this request
cookieStore()                         // see cookie store section below
skipRemainingHandlers()               // skip all remaining handlers for this request

App data

App data can be registered on the Javalin instance through Javalin#create, then accessed through the appData(...) method in Context. You need to create a typed key for your data, and then register it on the Javalin instance.

// register a custom attribute
var myKey = new Key<MyValue>("my-key");
var app = Javalin.create(config -> {
    config.appData(myKey, myValue);
});
// access a custom attribute
var myValue = ctx.appData(myKey); // var will be inferred to MyValue
// call a custom method on a custom attribute
ctx.appData(myKey).myMethod();
// register a custom attribute
val myKey = Key<MyValue>("my-key")
val app = Javalin.create { config ->
    config.appData(myKey, myValue)
}
// access a custom attribute
val myValue = ctx.appData(myKey) // val will be inferred to MyValue
// call a custom method on a custom attribute
ctx.appData(myKey).myMethod()

It can be helpful to store the key as a static field, but you can also recreate the key every time you need it.

The CookieStore class provides a convenient way for sharing information between handlers, request, or even servers:

ctx.cookieStore().set(key, value); // store any type of value
ctx.cookieStore().get(key);        // read any type of value
ctx.cookieStore().clear();         // clear the cookie-store

The cookieStore works like this:

  1. The first handler that matches the incoming request will populate the cookie-store-map with the data currently stored in the cookie (if any).
  2. This map can now be used as a state between handlers on the same request-cycle, pretty much in the same way as ctx.attribute()
  3. At the end of the request-cycle, the cookie-store-map is serialized, base64-encoded and written to the response as a cookie. This allows you to share the map between requests and servers (in case you are running multiple servers behind a load-balancer)
Example:
serverOneApp.post("/cookie-storer", ctx -> {
    ctx.cookieStore().set("string", "Hello world!");
    ctx.cookieStore().set("i", 42);
    ctx.cookieStore().set("list", Arrays.asList("One", "Two", "Three"));
});
serverTwoApp.get("/cookie-reader", ctx -> { // runs on a different server than serverOneApp
    String string = ctx.cookieStore().get("string")
    int i = ctx.cookieStore().get("i")
    List<String> list = ctx.cookieStore().get("list")
});
serverOneApp.post("/cookie-storer") { ctx ->
    ctx.cookieStore().set("string", "Hello world!")
    ctx.cookieStore().set("i", 42)
    ctx.cookieStore().set("list", listOf("One", "Two", "Three"))
}
serverTwoApp.get("/cookie-reader") { ctx -> // runs on a different server than serverOneApp
    val string = ctx.cookieStore().get("string")
    val i = ctx.cookieStore().get("i")
    val list = ctx.cookieStore().get("list")
}

Since the client stores the cookie, the get request to serverTwoApp will be able to retrieve the information that was passed in the post to serverOneApp.

Please note that cookies have a max-size of 4kb.

WebSockets

Javalin has a very intuitive way of handling WebSockets. You declare an endpoint with a path and configure the different event handlers in a lambda:

app.ws("/websocket/{path}", ws -> {
    ws.onConnect(ctx -> System.out.println("Connected"));
});
app.ws("/websocket/{path}") { ws ->
    ws.onConnect { ctx -> println("Connected") }
}

There are a total of five events supported:

ws.onConnect(WsConnectContext)
ws.onError(WsErrorContext)
ws.onClose(WsCloseContext)
ws.onMessage(WsMessageContext)
ws.onBinaryMessage(WsBinaryMessageContext)

The different flavors of WsContext expose different things, for example, WsMessageContext has the method .message() which gives you the message that the client sent. The differences between the different contexts is small, and a full overview can be seen in the WsContext section.

You can learn about how Javalin handles WebSocket concurrency in FAQ - Concurrency.

WsBefore

The app.wsBefore adds a handler that runs before a WebSocket handler. You can have as many before-handlers as you want per WebSocket endpoint, and all events are supported.

app.wsBefore(ws -> {
    // runs before all WebSocket requests
});
app.wsBefore("/path/*", ws -> {
    // runs before websocket requests to /path/*
});
app.wsBefore { ws ->
    // runs before all WebSocket requests
}
app.wsBefore("/path/*") { ws ->
    // runs before websocket requests to /path/*
}

WsEndpoint

A WebSocket endpoint is declared with app.ws(path, handler). WebSocket handlers require unique paths.

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.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") }
}

WsAfter

The app.wsAfter adds a handler that runs after a WebSocket handler. You can have as many after-handlers as you want per WebSocket endpoint, and all events are supported.

app.wsAfter(ws -> {
    // runs after all WebSocket requests
});
app.wsAfter("/path/*", ws -> {
    // runs after websocket requests to /path/*
});
app.wsAfter { ws ->
    // runs after all WebSocket requests
}
app.wsAfter("/path/*") { ws ->
    // runs after websocket requests to /path/*
}

WsContext

The WsContext object provides you with everything you need to handle a websocket-request. It contains the underlying websocket session and servlet-request, and convenience methods for sending messages to the client.

// Session methods
send(obj)                               // serialize object to json string and send it to client
send("message")                         // send string to client
send(byteBuffer)                        // send bytes to client
sendAsClass(obj, clazz)                 // serialize object to json string and send it to client

// Upgrade Context methods (getters)
matchedPath()                           // get the path that was used to match this request (ex, "/hello/{name}")
host()                                  // host as string

queryParam("name")                      // query param by name as string
queryParamAsClass("name", clazz)        // query param parameter by name, as validator typed as specified class
queryParams("name")                     // list of query parameters by name
queryParamMap()                         // map of all query parameters
queryString()                           // full query string

pathParam("name")                       // path parameter by name as string
pathParamAsClass("name", clazz)         // path parameter as validator typed as specified class
pathParamMap()                          // map of all path parameters

header("name")                          // request header by name (can be used with Header.HEADERNAME)
headerAsClass("name", clazz)            // request header by name, as validator typed as specified class
headerMap()                             // map of all request headers

cookie("name")                          // request cookie by name
cookieMap()                             // map of all request cookies

attribute("name", value)                // set an attribute on the request
attribute("name")                       // get an attribute on the request
attributeMap()                          // map of all attributes on the request

sessionAttribute("name")                // get a session attribute
sessionAttributeMap()                   // map of all session attributes

sendPing()                              // send a ping to the client
sendPing(bytes)                         // send a ping with data to the client
enableAutomaticPings()                  // enable automatic pinging to avoid timeouts
enableAutomaticPings(1, HOURS, bytes)   // enable automatic pinging with custom interval and/or data
disableAutomaticPings()                 // disable automatic pinging

closeSession()                          // close the session
closeSession(closeStatus)               // close the session with a CloseStatus
closeSession(400, "reason")             // close the session with a status and reason

WsMessageContext

message()                               // receive a string message from the client
messageAsClass(clazz)                   // deserialize message from client

WsBinaryMessageContext

data()                                  // receive a byte array of data from the client
offset()                                // the offset of the data
length()                                // the length of the data

WsCloseContext

status()                                // the int status for why connection was closed
reason()                                // the string reason for why connection was closed

WsErrorContext

error()                                 // the throwable error that occurred

WsConnectContext

The WsConnectContext class doesn’t add anything to the base WsContext

Handler groups

You can group your endpoints by using the apiBuilder() and path() methods. The apiBuilder() methods creates a temporary static instance of Javalin, so that you can skip the app. or router. prefix before your handlers. This is equivalent to calling ApiBuilder.get(app/router, ...), which translates to app/router.get(...). It is not a global singleton that holds static information, so you can use this safely in multiple locations and from multiple threads.

You can import all the HTTP methods with import static io.javalin.apibuilder.ApiBuilder.*.

config.router.apiBuilder(() -> {
    path("/users", () -> {
        get(UserController::getAllUsers);
        post(UserController::createUser);
        path("/{id}", () -> {
            get(UserController::getUser);
            patch(UserController::updateUser);
            delete(UserController::deleteUser);
        });
        ws("/events", UserController::webSocketEvents);
    });
});
config.router.apiBuilder {
    path("/users") {
        get(UserController::getAllUsers)
        post(UserController::createUser)
        path("/{id}") {
            get(UserController::getUser)
            patch(UserController::updateUser)
            delete(UserController::deleteUser)
        }
        ws("/events", UserController::webSocketEvents)
    }
}

Note that path() prefixes your paths with / (if you don’t add it yourself).
This means that path("api", ...) and path("/api", ...) are equivalent.

CrudHandler

The CrudHandler is an interface that can be used within an apiBuilder() call:

config.router.apiBuilder(() -> {
    crud("users/{user-id}", new UserController());
});
config.router.apiBuilder {
    crud("users/{user-id}", UserController())
}

It implements the five most common crud operations:

interface CrudHandler {
    getAll(ctx)
    getOne(ctx, resourceId)
    create(ctx)
    update(ctx, resourceId)
    delete(ctx, resourceId)
}

Validation

You can use Javalin’s Validator class for query, form, and path parameters, as well as headers and the request body:

ctx.queryParamAsClass("paramName", MyClass.class)   // creates a Validator<MyClass> for the value of queryParam("paramName")
ctx.formParamAsClass("paramName", MyClass.class)    // creates a Validator<MyClass> for the value of formParam("paramName")
ctx.pathParamAsClass("paramName", MyClass.class)    // creates a Validator<MyClass> for the value of pathParam("paramName")
ctx.headerAsClass("headerName", MyClass.class)      // creates a Validator<MyClass> for the value of header("paramName")
ctx.bodyValidator(MyClass.class)                    // creates a Validator<MyClass> for the value of body()
ctx.queryParamAsClass<MyClass>("paramName")         // creates a Validator<MyClass> for the value of queryParam("paramName")
ctx.formParamAsClass<MyClass>("paramName")          // creates a Validator<MyClass> for the value of formParam("paramName")
ctx.pathParamAsClass<MyClass>("paramName")          // creates a Validator<MyClass> for the value of pathParam("paramName")
ctx.headerAsClass<MyClass>("headerName")            // creates a Validator<MyClass> for the value of header("paramName")
ctx.bodyValidator<MyClass>()                        // creates a Validator<MyClass> for the value of body()

You can also create your own validator manually through Validator.create(clazz, value, fieldName).

Validator API

allowNullable()                     // turn the Validator into a NullableValidator (must be called first)
check(predicate, "error")           // add a check with a ValidationError("error") to the Validator
check(predicate, validationError)   // add a check with a ValidationError to the Validator (can have args for localization)
get()                               // return the validated value as the specified type, or throw ValidationException
getOrThrow(exceptionFunction)       // return the validated value as the specified type, or throw custom exception
getOrDefault()                      // return default-value if value is null, else call get()
errors()                            // get all the errors of the Validator (as map("fieldName", List<ValidationError>))

Validation examples

// VALIDATE A SINGLE QUERY PARAMETER WITH A DEFAULT VALUE /////////////////////////////////////////////
Integer myValue = ctx.queryParamAsClass("value", Integer.class).getOrDefault(788) // validate value
ctx.result(value) // return validated value to the client
// GET ?value=a would yield HTTP 400 - {"my-qp":[{"message":"TYPE_CONVERSION_FAILED","args":{},"value":"a"}]}
// GET ?value=1 would yield HTTP 200 - 1 (the validated value)
// GET ?        would yield HTTP 200 - 788 (the default value)


// VALIDATE TWO DEPENDENT QUERY PARAMETERS ////////////////////////////////////////////////////////////
Instant fromDate = ctx.queryParamAsClass("from", Instant.class).get();
Instant toDate = ctx.queryParamAsClass("to", Instant.class)
    .check(it -> it.isAfter(fromDate), "'to' has to be after 'from'")
    .get();


// VALIDATE A JSON BODY ///////////////////////////////////////////////////////////////////////////////
MyObject myObject = ctx.bodyValidator(MyObject.class)
    .check(obj -> obj.myObjectProperty == someValue, "THINGS_MUST_BE_EQUAL")
    .get();

// VALIDATE WITH CUSTOM VALIDATIONERROR ///////////////////////////////////////////////////////////////
ctx.queryParamAsClass("param", Integer.class)
    .check({ it > 5 }, new ValidationError("OVER_LIMIT", Map.of("limit", 5)))
    .get();
// GET ?param=10 would yield HTTP 400 - {"param":[{"message":"OVER_LIMIT","args":{"limit":5},"value":10}]}
// VALIDATE A SINGLE QUERY PARAMETER WITH A DEFAULT VALUE /////////////////////////////////////////////
val myValue = ctx.queryParamAsClass<Int>("value").getOrDefault(788) // validate value
ctx.result(value) // return validated value to the client
// GET ?value=a would yield HTTP 400 - {"my-qp":[{"message":"TYPE_CONVERSION_FAILED","args":{},"value":"a"}]}
// GET ?value=1 would yield HTTP 200 - 1 (the validated value)
// GET ?        would yield HTTP 200 - 788 (the default value)


// VALIDATE TWO DEPENDENT QUERY PARAMETERS ////////////////////////////////////////////////////////////
val fromDate = ctx.queryParamAsClass<Instant>("from").get()
val toDate = ctx.queryParamAsClass<Instant>("to")
    .check({ it.isAfter(fromDate) }, "'to' has to be after 'from'")
    .get()


// VALIDATE A JSON BODY ///////////////////////////////////////////////////////////////////////////////
val myObject = ctx.bodyValidator<MyObject>()
    .check({ it.myObjectProperty == someValue }, "THINGS_MUST_BE_EQUAL")
    .get()

// VALIDATE WITH CUSTOM VALIDATIONERROR ///////////////////////////////////////////////////////////////
ctx.queryParamAsClass<Int>("param")
    .check({ it > 5 }, ValidationError("OVER_LIMIT", args = mapOf("limit" to 5)))
    .get()
// GET ?param=10 would yield HTTP 400 - {"param":[{"message":"OVER_LIMIT","args":{"limit":5},"value":10}]}

Collecting multiple errors

Validator<Integer> ageValidator = ctx.queryParamAsClass("age", Integer.class)
    .check(n -> !n.contains("-"), "ILLEGAL_CHARACTER")

// Empty map if no errors, otherwise a map with the key "age" and failed check messages in the list.
Map<String, List<Integer>> errors = ageValidator.errors();

// Merges all errors from all validators in the list. Empty map if no errors exist.
Map<String, List<Object>> manyErrors = JavalinValidation.collectErrors(ageValidator, otherValidator, ...)
val ageValidator = ctx.queryParamAsClass<Int>("age")
    .check({ !it.contains("-") }, "ILLEGAL_CHARACTER")

// Empty map if no errors, otherwise a map with the key "age" and failed check messages in the list.
val errors = ageValidator.errors()

// Merges all errors from all validators in the list. Empty map if no errors exist.
val manyErrors = listOf(ageValidator, otherValidator, ...).collectErrors()

ValidationException

When a Validator throws, it is mapped by:

app.exception(ValidationException::class.java) { e, ctx ->
    ctx.json(e.errors).status(400)
}

You can override this by doing:

app.exception(ValidationException.class, (e, ctx) -> {
    // your code
});
app.exception(ValidationException::class.java) { e, ctx ->
    // your code
}

Custom converters

If you need to validate a non-included class, you have to register a custom converter:

Javalin.create(config -> {
    config.validation.register(Instant.class, v -> Instant.ofEpochMilli(Long.parseLong(v)));
});
Javalin.create { config ->
    config.validation.register(Instant::class.java) { Instant.ofEpochMilli(it.toLong()) }
}

Access management

Javalin used to have a functional interface AccessManager, which let you set per-endpoint authentication and/or authorization. In Javalin 6, this has been replaced with the beforeMatched handler. You can read more about this in the Javalin 5 to 6 migration guide.

To manage access in Javalin 6, you would do something like this:

app.beforeMatched(ctx -> {
    var userRole = getUserRole(ctx); // some user defined function that returns a user role
    if (!ctx.routeRoles().contains(userRole)) { // routeRoles are provided through the Context interface
        throw new UnauthorizedResponse(); // request will have to be explicitly stopped by throwing an exception
    }
});
app.beforeMatched { ctx ->
    val userRole = getUserRole(ctx) // some user defined function that returns a user role
    if (!ctx.routeRoles().contains(userRole)) { // routeRoles are provided through the Context interface
        throw UnauthorizedResponse() // request will have to be explicitly stopped by throwing an exception
    }
}

The routes are set when you declare your endpoints:

app.get("/public", ctx -> ctx.result("Hello public"), Role.OPEN);
app.get("/private", ctx -> ctx.result("Hello private"), Role.LOGGED_IN);
app.get("/public", { ctx -> ctx.result("Hello public") }, Role.OPEN)
app.get("/private", { ctx -> ctx.result("Hello private") }, Role.LOGGED_IN)

Default responses

Javalin comes with a built in class called HttpResponseException, which can be used for default responses. If the client accepts JSON, a JSON object is returned. Otherwise a plain text response is returned.

app.post("/") { throw new ForbiddenResponse("Off limits!") }

If client accepts JSON:

{
    "title": "Off limits!",
    "status": 403,
    "type": "https://javalin.io/documentation#forbiddenresponse",
    "details": []
}

Otherwise:

Forbidden

You can include a Map<String, String> of details if you wish.

RedirectResponse

Returns a 302 Found response with the default title Redirected.

BadRequestResponse

Returns a 400 Bad Request response with the default title Bad request.

UnauthorizedResponse

Returns a 401 Unauthorized response with the default title Unauthorized.

ForbiddenResponse

Returns a 403 Forbidden response with the default title Forbidden.

NotFoundResponse

Returns a 404 Not Found response with the default title Not found.

MethodNotAllowedResponse

Returns a 405 Method Not Allowed response with the default title Method not allowed.

ConflictResponse

Returns a 409 Conflict response with the default title Conflict.

GoneResponse

Returns a 410 Gone response with the default title Gone.

InternalServerErrorResponse

Returns a 500 Internal Server Error response with the default title Internal server error.

BadGatewayResponse

Returns a 502 Bad Gateway response with the default title Bad gateway.

ServiceUnavailableResponse

Returns a 503 Service Unavailable response with the default title Service unavailable.

GatewayTimeoutResponse

Returns a 504 Gateway Timeout response with the default title Gateway timeout.

Exception Mapping

All handlers (before, endpoint, after, ws) can throw Exception (and any subclass of Exception). The app.exception() and app.wsException() methods gives you a way of handling these exceptions:

// HTTP exceptions
app.exception(NullPointerException.class, (e, ctx) -> {
    // handle nullpointers here
});

app.exception(Exception.class, (e, ctx) -> {
    // handle general exceptions here
    // will not trigger if more specific exception-mapper found
});

// WebSocket exceptions
app.wsException(NullPointerException.class, (e, ctx) -> {
    // handle nullpointers here
});

app.wsException(Exception.class, (e, ctx) -> {
    // handle general exceptions here
    // will not trigger if more specific exception-mapper found
});
// HTTP exceptions
app.exception(NullPointerException::class.java) { e, ctx ->
    // handle nullpointers here
}

app.exception(Exception::class.java) { e, ctx ->
    // handle general exceptions here
    // will not trigger if more specific exception-mapper found
}

// WebSocket exceptions
app.wsException(NullPointerException::class.java) { e, ctx ->
    // handle nullpointers here
}

app.wsException(Exception::class.java) { e, ctx ->
    // handle general exceptions here
    // will not trigger if more specific exception-mapper found
}

Error Mapping

HTTP Error mapping is similar to exception mapping, but it operates on HTTP status codes instead of Exceptions:

app.error(404, ctx -> {
    ctx.result("Generic 404 message")
});
app.error(404) { ctx ->
    ctx.result("Generic 404 message")
}

It can make sense to use them together:

app.exception(FileNotFoundException.class, (e, ctx) -> {
    ctx.status(404);
}).error(404, ctx -> {
    ctx.result("Generic 404 message")
});
app.exception(FileNotFoundException::class.java) { e, ctx ->
    ctx.status(404)
}.error(404) { ctx ->
    ctx.result("Generic 404 message")
}

You can also include the content type when declaring your error mappers:

app.error(404, "html" ctx -> {
    ctx.html("Generic 404 message")
});
app.error(404, "html") { ctx ->
    ctx.html("Generic 404 message")
}

This can be useful if you, for example, want one set of error handlers for HTML, and one for JSON.

Server-sent Events

Server-sent events (often also called event source) are very simple in Javalin. You call app.sse(), which gives you access to the connected SseClient:

app.sse("/sse", client -> {
    client.sendEvent("connected", "Hello, SSE");
    client.onClose(() -> System.out.println("Client disconnected"));
    client.close(); // close the client
});
app.sse("/sse") { client ->
    client.sendEvent("connected", "Hello, SSE")
    client.onClose { println("Client disconnected") }
    client.close() // close the client
}

Clients are automatically closed when leaving the handler, if you need to use the client outside the handler, you can use client.keepAlive():

Queue<SseClient> clients = new ConcurrentLinkedQueue<SseClient>();

app.sse("/sse", client -> {
    client.keepAlive();
    client.onClose(() - > clients.remove(client));
    clients.add(client);
});
val clients = ConcurrentLinkedQueue<SseClient>()

app.sse("/sse") { client ->
    client.keepAlive()
    client.onClose { clients.remove(client) }
    clients.add(client)
}

SseClient API

sendEvent("myMessage")                      // calls emit("message", "myMessage", noId)
sendEvent("eventName", "myMessage")         // calls emit("eventName", "myMessage", noId)
sendEvent("eventName", "myMessage", "id")   // calls emit("eventName", "myMessage", "id")
sendComment("myComment")                    // calls emit("myComment")
onClose(runnable)                           // callback which runs when a client closes its connection
keepAlive()                                 // keeps the connection alive. useful if you want to keep a list of clients to broadcast to.
close()                                     // closes the connection
terminated()                                // returns true if the connection has been closed
ctx                                         // the Context from when the client connected (to fetch query-params, etc)

Configuration

You can pass a config object when creating a new instance of Javalin. Most of Javalin’s configuration is available through subconfigs, but there are also a few direct properties and functions:

Javalin.create(config -> {
    config.http // The http layer configuration: etags, request size, timeout, etc
    config.router // The routing configuration: context path, slash treatment, etc
    config.jetty // The embedded Jetty webserver configuration
    config.staticFiles // Static files and webjars configuration
    config.spaRoot = // Single Page Application roots configuration
    config.requestLogger // Request Logger configuration: http and websocket loggers
    config.bundledPlugins // Bundled plugins configuration: enable bundled plugins or add custom ones
    config.events // Events configuration
    config.vue // Vue Plugin configuration
    config.contextResolver // Context resolver implementation configuration
    config.validation // Default validator configuration
    config.useVirtualThreads // Use virtual threads (based on Java Project Loom)
    config.showJavalinBanner // Show the Javalin banner in the logs
    config.startupWatcherEnabled // Print warning if instance was not started after 5 seconds
    config.pvt // This is "private", only use it if you know what you're doing

    config.events(listenerConfig) // Add an event listener
    config.jsonMapper(jsonMapper) // Set a custom JsonMapper
    config.fileRenderer(fileRenderer) // Set a custom FileRenderer
    config.registerPlugin(plugin) // Register a plugin
    config.appData(key, data) // Store data on the Javalin instance
});

All available subconfigs are explained in the sections below.

HttpConfig

Javalin.create(config -> {
    config.http.generateEtags = booleanValue;       // if javalin should generate etags for dynamic responses (not static files)
    config.http.prefer405over404 = booleanValue;    // return 405 instead of 404 if path is mapped to different HTTP method
    config.http.maxRequestSize = longValue;         // the max size of request body that can be accessed without using using an InputStream
    config.http.defaultContentType = stringValue;   // the default content type
    config.http.asyncTimeout = longValue;           // timeout in milliseconds for async requests (0 means no timeout)
    
    config.http.customCompression(strategy);        // set a custom compression strategy
    config.http.brotliAndGzipCompression(lvl, lvl); // enable brotli and gzip compression with the specified levels
    config.http.gzipOnlyCompression(lvl);           // enable gzip compression with the specified level
    config.http.brotliOnlyCompression(lvl);         // enable brotli compression with the specified level
    config.http.disableCompression();               // disable compression
});
Javalin.create { config ->
    config.http.generateEtags = booleanValue        // if javalin should generate etags for dynamic responses (not static files)
    config.http.prefer405over404 = booleanValue     // return 405 instead of 404 if path is mapped to different HTTP method
    config.http.maxRequestSize = longValue          // the max size of request body that can be accessed without using using an InputStream
    config.http.defaultContentType = stringValue    // the default content type
    config.http.asyncTimeout = longValue            // timeout in milliseconds for async requests (0 means no timeout)

    config.http.customCompression(strategy)         // set a custom compression strategy
    config.http.brotliAndGzipCompression(lvl, lvl)  // enable brotli and gzip compression with the specified levels
    config.http.gzipOnlyCompression(lvl)            // enable gzip compression with the specified level
    config.http.brotliOnlyCompression(lvl)          // enable brotli compression with the specified level
    config.http.disableCompression()                // disable compression
}

ContextResolvers

Some of the methods in Context can be configured through the ContextResolvers configuration class:

Javalin.create(config -> {
    config.contextResolver.ip = ctx -> "custom ip";           // called by Context#ip()
    config.contextResolver.host = ctx -> "custom host";       // called by Context#host()
    config.contextResolver.scheme = ctx -> "custom scheme";   // called by Context#scheme()
    config.contextResolver.url = ctx -> "custom url";         // called by Context#url()
    config.contextResolver.fullUrl = ctx -> "custom fullUrl"; // called by Context#fullUrl()
});
Javalin.create { config ->
    config.contextResolver.ip = { ctx -> "custom ip" }           // called by Context#ip()
    config.contextResolver.host = { ctx -> "custom host" }       // called by Context#host()
    config.contextResolver.scheme = { ctx -> "custom scheme" }   // called by Context#scheme()
    config.contextResolver.url = { ctx -> "custom url" }         // called by Context#url()
    config.contextResolver.fullUrl = { ctx -> "custom fullUrl" } // called by Context#fullUrl()
}

JettyConfig

Javalin.create(config -> {
    config.jetty.defaultHost = "localhost"; // set the default host for Jetty
    config.jetty.defaultPort = 1234; // set the default port for Jetty
    config.jetty.threadPool = new ThreadPool(); // set the thread pool for Jetty
    config.jetty.multipartConfig = new MultipartConfig(); // set the multipart config for Jetty
    config.jetty.modifyJettyWebSocketServletFactory(factory -> {}); // modify the JettyWebSocketServletFactory
    config.jetty.modifyServer(server -> {}); // modify the Jetty Server
    config.jetty.modifyServletContextHandler(handler -> {}); // modify the ServletContextHandler (you can set a SessionHandler here)
    config.jetty.modifyHttpConfiguration(httpConfig -> {}); // modify the HttpConfiguration
    config.jetty.addConnector((server, httpConfig) -> new ServerConnector(server)); // add a connector to the Jetty Server
});
Javalin.create { config ->
    config.jetty.defaultHost = "localhost" // set the default host for Jetty
    config.jetty.defaultPort = 1234 // set the default port for Jetty
    config.jetty.threadPool = ThreadPool() // set the thread pool for Jetty
    config.jetty.multipartConfig = MultipartConfig() // set the multipart config for Jetty
    config.jetty.modifyJettyWebSocketServletFactory { factory -> } // modify the JettyWebSocketServletFactory
    config.jetty.modifyServer { server -> } // modify the Jetty Server
    config.jetty.modifyServletContextHandler { handler -> } // modify the ServletContextHandler (you can set a SessionHandler here)
    config.jetty.modifyHttpConfiguration { httpConfig -> } // modify the HttpConfiguration
    config.jetty.addConnector { server, httpConfig -> ServerConnector(server) } // add a connector to the Jetty Server
}

MultipartConfig

Keywords for ctrl+f: FileUploadConfig

Javalin uses standard servlet file upload handling to deal with multipart requests. This allows for configuring the maximum size for each individual file, the maximum size for the entire request, the maximum size of file to handle via in-memory upload and the cache directory to write uploaded files to if they exceed this limit.

All of these values can be configured through the config as follows

Javalin.create(config -> {
  config.jetty.multipartConfig.cacheDirectory("c:/temp"); //where to write files that exceed the in memory limit
  config.jetty.multipartConfig.maxFileSize(100, SizeUnit.MB); //the maximum individual file size allowed
  config.jetty.multipartConfig.maxInMemoryFileSize(10, SizeUnit.MB); //the maximum file size to handle in memory
  config.jetty.multipartConfig.maxTotalRequestSize(1, SizeUnit.GB); //the maximum size of the entire multipart request
});
Javalin.create { config ->
  config.jetty.multipartConfig.cacheDirectory("c:/temp") //where to write files that exceed the in memory limit
  config.jetty.multipartConfig.maxFileSize(100, SizeUnit.MB) //the maximum individual file size allowed
  config.jetty.multipartConfig.maxInMemoryFileSize(10, SizeUnit.MB) //the maximum file size to handle in memory
  config.jetty.multipartConfig.maxTotalRequestSize(1, SizeUnit.GB) //the maximum size of the entire multipart request
}

RequestLoggerConfig

You can add a HTTP request logger by calling config.requestLogger.http(). The method takes a Context and the time in milliseconds it took to finish the request:

Javalin.create(config -> {
    config.requestLogger.http((ctx, ms) -> {
        // log things here
    });
});
Javalin.create { config ->
    config.requestLogger.http { ctx, ms ->
        // log things here
    }
}

You can add a WebSocket logger by calling config.requestLogger.ws(). The method takes a the same arguments as a normal app.ws() call, and can be used to log events of all types. The following example just shows onMessage, but onConnect, onError and onClose are all available:

Javalin.create(config -> {
    config.requestLogger.ws(ws -> {
        ws.onMessage(ctx -> {
            System.out.println("Received: " + ctx.message());
        });
    });
});
Javalin.create { config ->
    config.requestLogger.ws(ws -> {
        ws.onMessage { ctx ->
            println("Received: " + ctx.message());
        }
    }
}

The logger runs after the WebSocket handler for the endpoint.

RouterConfig

Javalin.create(config -> {
    config.router.contextPath = stringValue; // the context path (ex '/blog' if you are hosting an app on a subpath, like 'mydomain.com/blog')
    config.router.ignoreTrailingSlashes = booleanValue; // treat '/path' and '/path/' as the same path
    config.router.treatMultipleSlashesAsSingleSlash = booleanValue; // treat '/path//subpath' and '/path/subpath' as the same path
    config.router.caseInsensitiveRoutes = booleanValue; // treat '/PATH' and '/path' as the same path
});
Javalin.create { config ->
    config.router.contextPath = stringValue // the context path (ex '/blog' if you are hosting an app on a subpath, like 'mydomain.com/blog')
    config.router.ignoreTrailingSlashes = booleanValue // treat '/path' and '/path/' as the same path
    config.router.treatMultipleSlashesAsSingleSlash = booleanValue // treat '/path//subpath' and '/path/subpath' as the same path
    config.router.caseInsensitiveRoutes = booleanValue // treat '/PATH' and '/path' as the same path
}

SpaRootConfig

Single page application (SPA) mode is similar to static file handling. It runs after endpoint matching, and after static file handling. It’s basically a very fancy 404 mapper, which converts any 404’s into a specified page. You can define multiple single page handlers for your application by specifying different root paths.

You can enabled single page mode by doing config.spaRoot.addFile("/root", "/path/to/file.html"), and/or config.spaRoot.addFile("/root", "/path/to/file.html", Location.EXTERNAL).

Dynamic single page handler

You can also use a Handler to serve your single page root (as opposed to a static file):

config.spaRoot.addHandler("/root",  ctx -> {
    ctx.html(...);
});

StaticFileConfig

You can enable static file serving by doing config.staticFiles.add("/directory", location). Static resource handling is done after endpoint matching, meaning your own GET endpoints have higher priority. The process looks like this:

run before-handlers
run endpoint-handlers
if no endpoint-handler found
    run static-file-handlers
    if static-file-found
        static-file-handler sends response
    else
        response is 404
run after-handlers

For more advanced use cases, Javalin has a StaticFileConfig class:

Javalin.create(config -> {
  config.staticFiles.add(staticFiles -> {
    staticFiles.hostedPath = "/";                   // change to host files on a subpath, like '/assets'
    staticFiles.directory = "/public";              // the directory where your files are located
    staticFiles.location = Location.CLASSPATH;      // Location.CLASSPATH (jar) or Location.EXTERNAL (file system)
    staticFiles.precompress = false;                // if the files should be pre-compressed and cached in memory (optimization)
    staticFiles.aliasCheck = null;                  // you can configure this to enable symlinks (= ContextHandler.ApproveAliases())
    staticFiles.headers = Map.of(...);              // headers that will be set for the files
    staticFiles.skipFileFunction = req -> false;    // you can use this to skip certain files in the dir, based on the HttpServletRequest
    staticFiles.mimeTypes.add(mimeType, ext);       // you can add custom mimetypes for extensions
  });
});
Javalin.create { config ->
  config.staticFiles.add { staticFiles ->
    staticFiles.hostedPath = "/"                    // change to host files on a subpath, like '/assets'
    staticFiles.directory = "/public"               // the directory where your files are located
    staticFiles.location = Location.CLASSPATH       // Location.CLASSPATH (jar) or Location.EXTERNAL (file system)
    staticFiles.precompress = false                 // if the files should be pre-compressed and cached in memory (optimization)
    staticFiles.aliasCheck = null                   // you can configure this to enable symlinks (= ContextHandler.ApproveAliases())
    staticFiles.headers = mapOf(...)                // headers that will be set for the files
    staticFiles.skipFileFunction = { req -> false } // you can use this to skip certain files in the dir, based on the HttpServletRequest
    staticFiles.mimeTypes.add(mimeType, ext)        // you can add custom mimetypes for extensions
  }
}

You can call config.staticFiles.add(...) multiple times to set up multiple handlers. No configuration is shared between handlers.

WebJars can be enabled by calling config.staticFiles.enableWebjars(), they will be available at /webjars/name/version/file.ext. WebJars can be found on https://www.webjars.org/. Everything available through NPM is also available through WebJars.

If you are building a Single Page Application (SPA), you should have a look at the SpaRootConfig

Logging

Adding a logger

Javalin does not have a logger included, which means that you have to add your own logger. If you don’t know/care a lot about Java loggers, the easiest way to fix this is to add the following dependency to your project:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>2.0.11</version>
</dependency>

Server setup

Javalin runs on an embedded Jetty. To start and stop the server, use start() and stop:

Javalin app = Javalin.create()
    .start() // start server (sync/blocking)
    .stop() // stop server (sync/blocking)

The app.start() method spawns a user thread, starts the server, and then returns. Your program will not exit until this thread is terminated by calling app.stop().

If you want to do a clean shutdown when the program is exiting, you could use:

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
	app.stop();
}));

app.events(event -> {
    event.serverStopping(() -> { /* Your code here */ });
    event.serverStopped(() -> { /* Your code here */ });
});

Setting the Host

The Javalin#start method is overloaded to accept the Host (IP) as the first argument:

Javalin.create().start("127.0.0.1", 1235)

Custom SessionHandler

Read about how to configure sessions in our session tutorial.

Custom jetty handlers

You can configure your embedded jetty-server with a handler-chain (example), and Javalin will attach it’s own handlers to the end of this chain.

StatisticsHandler statisticsHandler = new StatisticsHandler();

Javalin.create(config -> {
    config.server(() -> {
        Server server = new Server();
        server.setHandler(statisticsHandler);
        return server;
    })
});
val statisticsHandler = StatisticsHandler()

Javalin.create { config ->
    config.server {
        Server().apply {
            handler = statisticsHandler
        }
    }
}.start();

SSL/HTTP2

Javalin now has a SSL plugin: https://javalin.io/plugins/ssl-helpers. It’s recommended to use this plugin for setting up SSL and HTTP2/3, as it’s a lot more user-friendly than configuring it manually in Jetty.

To configure SSL or HTTP2 manually in Jetty you need to use a custom server.

An example of a custom server with SSL can be found in the examples, HelloWorldSecure.

A custom HTTP2 server is a bit more work to set up, but we have a repo with a fully functioning example server in both Kotlin and Java: javalin-http2-example

Lifecycle events

Javalin has events for server start/stop, as well as for when handlers are added. The snippet below shows all of them in action:

Javalin app = Javalin.create().events(event -> {
    event.serverStarting(() -> { ... });
    event.serverStarted(() -> { ... });
    event.serverStartFailed(() -> { ... });
    event.serverStopping(() -> { ... });
    event.serverStopped(() -> { ... });
    event.handlerAdded(handlerMetaInfo -> { ... });
    event.wsHandlerAdded(wsHandlerMetaInfo -> { ... });
});

app.start() // serverStarting -> (serverStarted || serverStartFailed)
app.stop() // serverStopping -> serverStopped
Javalin app = Javalin.create().events { event ->
    event.serverStarting { ... }
    event.serverStarted { ... }
    event.serverStartFailed { ... }
    event.serverStopping { ... }
    event.serverStopped { ... }
    event.handlerAdded { handlerMetaInfo -> }
    event.wsHandlerAdded { wsHandlerMetaInfo -> }
}

app.start() // serverStarting -> (serverStarted || serverStartFailed)
app.stop() // serverStopping -> serverStopped

Plugins

Javalin has a plugin system, which lets you add functionality to Javalin. You do this by extending the Plugin class and overriding the methods you’re interested in.

See the plugins page for more information about plugins.

FAQ

Frequently asked questions.

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)
Javalin#get/post/patch/etc  // runs second, can throw exception
Javalin#error               // runs third, can throw exception
Javalin#after               // runs fourth, can throw exception
Javalin#exception           // runs any time a handler throws (cannot throw exception)
JavalinConfig#requestLogger // runs after response is written to client
JavalinConfig#accessManager // wraps all your endpoint handlers in a lambda of your choice

Rate limiting

There is a very simple rate limiter included in Javalin. You can call it in the beginning of your endpoint Handler functions:

app.get("/", ctx -> {
    NaiveRateLimit.requestPerTimeUnit(ctx, 5, TimeUnit.MINUTES); // throws if rate limit is exceeded
    ctx.status("Hello, rate-limited World!");
});

// you can overwrite the key-function:
RateLimitUtil.keyFunction = ctx -> // uses (ip+method+endpointPath) by default
app.get("/") { ctx ->
    NaiveRateLimit.requestPerTimeUnit(ctx, 5, TimeUnit.MINUTES) // throws if rate limit is exceeded
    ctx.status("Hello, rate-limited World!")
}

// you can overwrite the key-function:
RateLimitUtil.keyFunction = { ctx -> } // uses (ip+method+endpointPath) by default

Different endpoints can have different rate limits. It works as follows:


Android

Due to Jetty 11 not working on Android, Javalin 5+ is not compatible either, but Javalin 4 is.
You can find the docs for Javalin 4 here.
You can check the status of Jetty 11+ on Android here.


Concurrency

If your JRE supports project Loom, Javalin will use a newVirtualThreadPerTaskExecutor for serving requests if you set the enableVirtualThreads config option. Otherwise, a QueuedThreadPool with 250 threads will be used.

Each incoming request is handled by a dedicated thread, so all Handler implementations should be thread-safe. This default configuration allows Javalin to handle up to 250 concurrent requests, which is generally more than enough (keep in mind, most requests are much shorter than 1 second).

If your application involves numerous long-running requests, consider exploring Asynchronous requests or investigate setting up Javalin with project Loom, detailed in the Loomylin repository.

In cases where you use ctx.future() or ctx.async(), the thread will be returned to the thread pool while the asynchronous task is running. Consequently, the request might be handled by a different thread when the asynchronous task is completed.

If you are uncertain whether your application requires asynchronous requests, it’s likely not necessary. The default configuration provides similar performance to Jetty, which can handle over a million plaintext requests per second.

WebSocket Message Ordering

WebSocket operates over TCP, so messages will arrive at the server in the order that they were sent by the client. Javalin then handles the messages from a given WebSocket connection sequentially. Therefore, the order that messages are handled is guaranteed to be the same as the order the client sent them in.

However, different connections will be handled in parallel on multiple threads, so the WebSocket event handlers should be thread-safe.


Testing

People often ask how to test Javalin apps. Since Javalin is just a library, you can instantiate and start the server programmatically. This means testing is really up to you. There is a tutorial at /tutorials/testing which goes through some different types of tests (unit tests, functional/integration tests, ui/end-to-end tests). You can read it to get some ideas for how to test your app.


Javadoc

There is a Javadoc available at javadoc.io. Please contribute to the Javadoc if you can.


Deploying

To deploy Javalin, simply create a jar with dependencies, then launch the jar with java -jar filename.jar. That’s it. Javalin has an embedded server, so you don’t need an application server.

Deploying Javalin to:


Other web servers

Ctrl+f: "without jetty", "tomcat", "standalone", "servlet container", "war".

Javalin is primarily meant to be used with the embedded Jetty server, but if you want to run Javalin on another web server (such as Tomcat), you can use Maven or Gradle to exclude Jetty, and attach Javalin as a servlet.


Uploads

Uploaded files are easily accessible via ctx.uploadedFiles():

app.post("/upload", ctx -> {
    ctx.uploadedFiles("files").forEach(uploadedFile ->
        FileUtil.streamToFile(uploadedFile.content(), "upload/" + uploadedFile.filename()));
});
app.post("/upload") { ctx ->
    ctx.uploadedFiles("files").forEach { uploadedFile ->
        FileUtil.streamToFile(uploadedFile.content(), "upload/${uploadedFile.filename()}")
    }
}

The corresponding HTML might look something like this:

<form method="post" action="/upload" enctype="multipart/form-data">
    <input type="file" name="files" multiple>
    <button>Submit</button>
</form>

Asynchronous requests

Synonyms for ctrl+f: Async, CompletableFuture, Future, Concurrent, Concurrency

While the default ThreadPool (250 threads) is enough for most use cases, sometimes slow operations should be run asynchronously. Luckily it’s very easy in Javalin, just pass a Supplier<CompletableFuture> to ctx.future(). Javalin will automatically switch between sync and async modes to handle the different tasks.

Using Futures

Let’s look at a real world example. Imagine that we have a random cat fact API that we want to call on behalf of a client. We’ll start by creating a simple method to call the API, which returns a CompletableFuture<String> which will resolve either to a cat fact or an error. This is possible by using Java’s native HttpClient:

private static CompletableFuture<HttpResponse<String>> getRandomCatFactFuture() {
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://catfact.ninja/fact"))
        .build();
    return httpClient.sendAsync(request, ofString());
}
private fun getRandomCatFactFuture(): CompletableFuture<HttpResponse<String>> {
    val request = HttpRequest.newBuilder()
        .uri(URI.create("https://catfact.ninja/fact"))
        .build()
    return httpClient.sendAsync(request, ofString())
)

Now we can use this method in our Javalin app to return cat facts to the client asynchronously:

app.get("/cat-facts", ctx -> {
    ctx.future(() -> {
        return getRandomCatFactFuture()
            .thenAccept(response -> ctx.html(response.body()).status(response.statusCode()))
            .exceptionally(throwable -> {
                ctx.status(500).result("Could not get cat facts" + throwable.getMessage());
                return null;
            })
    });
});
app.get("/cat-facts") { ctx ->
    ctx.future {
        getRandomCatFactFuture()
            .thenAccept { response -> ctx.html(response.body()).status(response.statusCode()) }
            .exceptionally { throwable ->
                ctx.status(500).result("Could not get cat facts: ${throwable.message}")
                null
            }
    }
}

By calling ctx.future(supplier) you are not putting Javalin in an async state. It’s a simple setter method, which makes it possible for Javalin to call the given supplier and switch into async mode at an appropriate time.

The ctx.future() method works great if you are using a CompletableFuture based API, like Java’s HttpClient, but if you have long running tasks which aren’t CompletableFuture based, you might want to try the ctx.async(runnable) (see next section).

Executing blocking tasks asynchronously

If you want to execute a blocking task outside of the server ThreadPool, you can use ctx.async(). The snippet below shows the available overloads for the method:

async(runnableTask)                               // Javalin's default executor, no timeout or timeout callback
async(timeout, onTimeout, runnableTask)           // Javalin's default executor, custom timeout handling
async(executor, timeout, onTimeout, runnableTask) // custom everything!

Javalin will immediately start an async context and run the task on a dedicated executor service. It will resume the normal request flow (after-handlers, request-logging) once the task is done.

The snippet belows shows a full example with a custom timeout, timeout handler, and a task:

app.get("/async", ctx -> {
    ctx.async(
        1000,                                      // timeout in ms
        () -> ctx.result("Request took too long"), // timeout callback
        () -> ctx.result(someSlowResult)           // some long running task
    );
});

app.get("/async") { ctx ->
    ctx.async(
        1000,                                    // timeout in ms
        { ctx.result("Request took too long") }, // timeout callback
        { ctx.result(someSlowResult)             // some long running task
    )
}

Configuring the JSON mapper

To configure the JsonMapper, you need to pass an object which implements the JsonMapper interface to config.jsonMapper().

The JsonMapper interface has four optional methods:

String toJsonString(Object obj, Type type) { // basic method for mapping to json
InputStream toJsonStream(Object obj, Type type) { // more memory efficient method for mapping to json
writeToOutputStream(Stream<*> stream, OutputStream outputStream) { // most memory efficient method for mapping to json
<T> T fromJsonString(String json, Type targetType) { // basic method for mapping from json
<T> T fromJsonStream(InputStream json, Type targetType) { // more memory efficient method for mapping from json

The default JSON mapper (Jackson)

Javalin uses Jackson as the default JSON mapper. It’s a fast and feature-rich mapper, and has the following modules enabled if they are available on the classpath:

com.fasterxml.jackson.module.kotlin.KotlinModule // Kotlin support
com.fasterxml.jackson.datatype.jsr310.JavaTimeModule // Java date/time support
org.ktorm.jackson.KtormModule // Ktorm support
com.fasterxml.jackson.datatype.eclipsecollections.EclipseCollectionsModule // Eclipse Collections support

If you need further config, you can update the default settings like this:

config.jsonMapper(new JavalinJackson().updateMapper(mapper -> {
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
});
config.jsonMapper(JavalinJackson().updateMapper { mapper ->
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
})

GSON example

Gson gson = new GsonBuilder().create();
JsonMapper gsonMapper = new JsonMapper() {
    @Override
    public String toJsonString(@NotNull Object obj, @NotNull Type type) {
        return gson.toJson(obj, type);
    }

    @Override
    public <T> T fromJsonString(@NotNull String json, @NotNull Type targetType) {
        return gson.fromJson(json, targetType);
    }
};
Javalin app = Javalin.create(config -> config.jsonMapper(gsonMapper)).start(7070);
val gson = GsonBuilder().create()

val gsonMapper = object : JsonMapper {

    override fun <T : Any> fromJsonString(json: String, targetType: Type): T =
        gson.fromJson(json, targetType)

    override fun toJsonString(obj: Any, type: Type) =
        gson.toJson(obj)

}

val app = Javalin.create { it.jsonMapper(gsonMapper) }.start(7070)

Adding other Servlets and Filters to Javalin

Javalin is designed to work with other Servlet and Filter instances running on the Jetty Server. Filters are pretty straighforward to add, since they don’t finish the request. If you need to add a serlvet there’s an example in the repo: /src/test/java/io/javalin/examples/HelloWorldServlet.java#L21-L29

You can also use it to build simple proxy using AsyncProxyServlet that is part of Jetty:

// Add org.eclipse.jetty:jetty-proxy to maven/gradle dependencies (e.g Javalin 5.3.2 uses Jetty 11.0.13)
Javalin.create(config -> {
    config.jetty.modifyServletContextHandler(handler -> {
        ServletHolder proxyServlet = new ServletHolder(AsyncProxyServlet.Transparent.class);
        proxyServlet.setInitParameter("proxyTo", "https://javalin.io/");
        proxyServlet.setInitParameter("prefix", "/proxy");
        handler.addServlet(proxyServlet, "/proxy/*");
    });
}).start(7000);

After opening http://localhost:7000/proxy/ you will see Javalin site (but with broken styles because of file paths).


Views and Templates

Each Javalin instance has a FileRenderer attached to it. The FileRenderer interface has one method:

String render(String filePath, Map<String, Object> model, Context context)

This method is called when you call Context#render. It can be configured through the config passed to Javalin.create():

config.fileRenderer((filePath, model, context) -> "Rendered template");
config.fileRenderer { filePath, model, context -> "Rendered template" }

The default FileRenderer of Javalin is a singleton named JavalinRenderer, see the section below for more information.

Default implementations

Javalin offers an artifact with several template engines, called javalin-rendering, which follows the same version as the javalin artifact. You can learn more about this at /plugins/rendering.


Vue support (JavalinVue)

If you don’t want to deal with NPM and frontend builds, Javalin has support for simplified Vue.js development. This requires you to make a layout template, src/main/resources/vue/layout.html:

<head>
    <script src="/webjars/vue/2.6.10/dist/vue.min.js"></script>
    @componentRegistration
</head>
<body>
<main id="main-vue" v-cloak>
    @routeComponent
</main>
<script>
    new Vue({el: "#main-vue"});
</script>
</body>

When you put .vue files in src/main/resources/vue, Javalin will scan the folder and register the components in your <head> tag.

Javalin will also put path-parameters in the Vue instance, which you can access like this:

<template id="thread-view">
    <div>{{ $javalin.pathParams["user"] }}</div>
</template>
<script>
    Vue.component("thread-view", {
        template: "#thread-view"
    });
</script>

To map a path to a Vue component you use the VueComponent class:

get("/messages",        VueComponent("inbox-view"))
get("/messages/{user}", VueComponent("thread-view"))

This will give you a lot of the benefits of a modern frontend architecture, with very few of the downsides.

There are more extensive docs at /plugins/javalinvue, and there is an in-depth tutorial at /tutorials/simple-frontends-with-javalin-and-vue.


Jetty debug logs

If you encounter TimeoutExceptions and ClosedChannelExceptions in your DEBUG logs, this is nothing to worry about. Typically, a browser will keep the HTTP connection open until the server terminates it. When this happens is decided by the server’s idleTimeout setting, which is 30 seconds by default in Jetty/Javalin. This is not a bug.


Java lang Error handling

Javalin has a default error handler for java.lang.Error that will log the error and return a 500. The default error handler can be overridden using the private config:

Javalin.create( cfg -> {
    cfg.pvt.javaLangErrorHandler((res, error) -> {
        res.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.getCode());
        JavalinLogger.error("Exception occurred while servicing http-request", error);
    });
});
Javalin.create { cfg ->
    cfg.pvt.javaLangErrorHandler { res, error ->
        res.status = HttpStatus.INTERNAL_SERVER_ERROR.code
        JavalinLogger.error("Exception occurred while servicing http-request", error)
    }
}

Minecraft

Keywords for ctrl+f: Bukkit, Spigot, BungeeCord, Bungee Cord, WaterFall, Water Fall, Paper

A lot of people use Javalin for Minecraft servers, and they often have issues with Jetty and WebSockets.

Please consider consulting our Minecraft tutorial if you’re working with Javalin and a Minecraft server.

Relocation

Use relocate is not required, but it can easily conflict with other plugin dependencies. If this is a publicly released plugin, this step is recommended to make Javalin work on a different Minecraft Server.

Usually jetty causes the conflict, you can add gradle script to build.gradle following after adding the shadow-jar gradle plugin:

shadowJar {
    relocate 'org.eclipse.jetty', 'shadow.org.eclipse.jetty'
}

Custom classloader

If you encounter some dependency missing errors such as java.lang.NoClassDefFoundError and java.lang.ClassNotFoundException, try to solve it by:

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(RemoteAPI.class.getClassLoader());
Javalin app = Javalin.create().start(PORT);
Thread.currentThread().setContextClassLoader(classLoader);

RemoteAPI can usually use the class loader of the main class of the plugin. On Bukkit and Spigot it is a class extends org.bukkit.plugin.java#JavaPlugin, on BungeeCord and WaterFall it is a class extends net.md_5.bungee.api.plugin#Plugin. Get it via {your plugin's main class}.class.getClassLoader() .

After switching the class loader, you may still receive a missing dependency error from Javalin. You only need to add the corresponding dependency as prompted in the Javalin log.

Relevant issues


Documentation for previous versions

Docs for 5.6.X (last 5.X version) can be found here.
Docs for 4.6.X (last 4.X version) can be found here.
Docs for 3.13.X (last 3.X version) can be found here.
Docs for 2.8.0 (last 2.X version) can be found here.
Docs for 1.7.0 (last 1.X version) can be found here.


You've reached the end of the docs, congratulations.

Like Javalin?
Star us 😊

×