Fork me on GitHub

Documentation - Javalin 3.X

This page contains documentation for an older version of Javalin. Go to javalin.io/documentation to view documentation for the newest version.

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>3.13.13</version>
</dependency>

Not familiar with Maven? Read our Maven tutorial.

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

Not familiar with Gradle? Read our Gradle tutorial.

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

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 ctx.result() to set the response which will be returned to the user.

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

Before handlers

Before-handlers are matched before every request (including static files, if you enable those).

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/*
}

Endpoint handlers

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

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

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

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

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

app.get("/hello/:name", ctx -> {
    ctx.result("Hello: " + ctx.pathParam("name"));
});
app.get("/hello/:name") { ctx ->
    ctx.result("Hello: " + ctx.pathParam("name"))
}

Handler-paths can also include wildcard parameters (splats). These are available via Context.splat()

app.get("/hello/*/and/*", ctx -> {
    ctx.result("Hello: " + ctx.splat(0) + " and " + ctx.splat(1));
});
app.get("/hello/*/and/*") { ctx ->
    ctx.result("Hello: " + ctx.splat(0) + " and " + ctx.splat(1))
}

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/*
}

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. The getters operate mostly on the request-object, while the setters operate exclusively on the response object.

ctx.appAttribute(class)                 // get an attribute set on the app
ctx.register(class, object)             // register an extension on the context
ctx.use(class)                          // use an extension on the context
ctx.cookieStore(key)                    // get cookie store value
ctx.cookieStore(key, value)             // set a cookie store value
ctx.clearCookieStore()                  // clear the cookie store
ctx.matchedPath()                       // path that was used to match request (also includes before/after paths)
ctx.endpointHandlerPath()               // endpoint path that was used to match request (null in before, available in after)

// Request methods
ctx.body()                              // get body as string (consumes underlying request body if not cached)
ctx.bodyAsBytes()                       // get body as bytes (consumes underlying request body if not cached)
ctx.bodyAsClass(class)                  // get body as class (consumes underlying request body if not cached)
ctx.bodyValidator(class)                // get typed validator for body (consumes underlying body request if not cached)
ctx.uploadedFile(name)                  // get uploaded file by name
ctx.uploadedFiles(name)                 // get uploaded file(s) by name
ctx.formParam(key)                      // get form parameter
ctx.formParam(key, default)             // get form parameter (or default value)
ctx.formParam(key, class)               // get form parameter as class
ctx.formParam(key, class, default)      // get form parameter (or default value) as class
ctx.formParams(key)                     // get form parameters (multiple)
ctx.formParamMap()                      // get form parameter map
ctx.pathParam(key)                      // get path parameter
ctx.pathParam(key, class)               // get path as class
ctx.pathParamMap()                      // get path parameter map
ctx.splat(0);                           // get splat by index, ex "/*" -> splat(0)
ctx.splats();                           // get array of splat-values
ctx.basicAuthCredentials()              // get basic auth credentials (username/pwd)
ctx.basicAuthCredentialsExist()         // check if proper basic auth credentials exist
ctx.attribute(key, value)               // set request attribute
ctx.attribute(key)                      // get request attribute
ctx.attributeMap()                      // get request attribute map
ctx.contentLength()                     // get request content length
ctx.contentType()                       // get request content type
ctx.cookie(name)                        // get request cookie
ctx.cookieMap()                         // get request cookie map
ctx.header(header)                      // get request header
ctx.headerMap()                         // get request header map
ctx.host()                              // get request host
ctx.ip()                                // get request ip address
ctx.isMultipart()                       // check if request is multipart
ctx.isMultipartFormData()               // check if request is multipart/form data
ctx.method()                            // get request method
ctx.path()                              // get request path
ctx.port()                              // get request port
ctx.protocol()                          // get request protocol
ctx.queryParam(key)                     // get query parameter
ctx.queryParam(key, default)            // get query parameter (or default value)
ctx.queryParam(key, class)              // get query parameter as class
ctx.queryParam(key, class, default)     // get query parameter (or default value) as class
ctx.queryParams(key)                    // get query parameters (multiple)
ctx.queryParamMap()                     // get query parameter map
ctx.queryString()                       // get query string
ctx.scheme()                            // get request scheme
ctx.sessionAttribute(key, value)        // set session attribute (server side attribute)
ctx.sessionAttribute(key)               // get session attribute
ctx.sessionAttributeMap()               // get attribute map
ctx.url()                               // get request url
ctx.fullUrl()                           // get request url + query param
ctx.contextPath()                       // get request context path
ctx.userAgent()                         // get request user agent

// Response methods
ctx.result(resultString)                // set a string result that will be sent to the client
ctx.resultString()                      // get the string result that will be sent to the client
ctx.result(resultStream)                // set a stream result that will be sent to the client
ctx.result(byteArray)                   // set a byte[] result that will be sent to the client
ctx.resultStream()                      // get the stream that will be sent to the client
ctx.result(future)                      // set a future result that will be sent to the client (async)
ctx.resultFuture()                      // get the future result that will be sent to the client
ctx.seekableStream(resultStream)        // set a stream that will be sent to the client in byte ranges
ctx.contentType(contentType)            // set the response content type
ctx.header(name, value)                 // set a response header
ctx.redirect(location)                  // send a redirect response to location
ctx.redirect(location, httpStatusCode)  // send a redirect response to location with status code
ctx.status(statusCode)                  // set response status
ctx.status()                            // get response status
ctx.cookie(name, value)                 // set cookie by name and value
ctx.cookie(cookie)                      // set cookie
ctx.removeCookie(name, path)            // remove a cookie
ctx.html(html)                          // call result(string).contentType("text/html")
ctx.json(obj)                           // call result(JavalinJson.toJson(obj)).contentType("application/json")
ctx.json(future)                        // call result(future(JavalinJson.toJson(future))).contentType("application/json")
ctx.render(filePath, model)             // call html(JavalinRenderer.render(filePath, model)

The ctx.cookieStore() functions provide a convenient way for sharing information between handlers, request, or even servers:

ctx.cookieStore(key, value); // store any type of value
ctx.cookieStore(key); // read any type of value
ctx.clearCookieStore(); // 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("string", "Hello world!");
    ctx.cookieStore("i", 42);
    ctx.cookieStore("list", Arrays.asList("One", "Two", "Three"));
});
serverTwoApp.get("/cookie-reader", ctx -> { // runs on a different server than serverOneApp
    String string = ctx.cookieStore("string")
    int i = ctx.cookieStore("i")
    List<String> list = ctx.cookieStore("list")
});
serverOneApp.post("/cookie-storer") { ctx ->
    ctx.cookieStore("string", "Hello world!")
    ctx.cookieStore("i", 42)
    ctx.cookieStore("list", listOf("One", "Two", "Three"))
}
serverTwoApp.get("/cookie-reader") { ctx -> // runs on a different server than serverOneApp
    val string = ctx.cookieStore<String>("string")
    val i = ctx.cookieStore<Int>("i")
    val list = ctx.cookieStore<List<String>>("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.

Context extensions

Context extensions give Java developers a way of extending the Context object.

One of the most popular features of Kotlin is extension functions. When working with an object you don’t own in Java, you often end up making MyUtil.action(object, ...). If you, for example, want to serialize an object and set it as the result on the Context, you might do:

app.get("/", ctx -> MyMapperUtil.serialize(ctx, myMapper, myObject)); // three args, what happens where?

With context extensions you can add custom extensions on the context:

app.get("/", ctx -> ctx.use(MyMapper.class).serialize(object)); // use MyMapper to serialize object

Context extensions have to be added before you can use them, this would typically be done in the first before filter of your app:

app.before(ctx -> ctx.register(MyMapper.class, new MyMapper(ctx, otherDependency));

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 more 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.message(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.message<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/: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.

ctx.matchedPath()            // get the path used to match this request, ex "/path/:param"

ctx.send(object)             // send an object as JSON (string message)
ctx.send(string)             // send a string message
ctx.send(byteBuffer)         // send a bytebuffer message

ctx.queryString()            // get the query string
ctx.queryParamMap()          // get a map of the query parameters
ctx.queryParams(key)         // get query parameters by key
ctx.queryParam(key)          // get query parameter by key
ctx.queryParam(key, default) // get query parameter (or default value)
ctx.queryParam(key, class)   // get query parameter as class

ctx.pathParamMap()           // get path parameter map
ctx.pathParam(key)           // get path parameter
ctx.pathParam(key, class)    // get path parameter as class

ctx.host()                   // get the host

ctx.header(key)              // get request header
ctx.headerMap()              // get a map of the request headers

ctx.cookie(key)              // get request cookie
ctx.cookieMap()              // get a map of all request cookies

ctx.attribute(key, value)    // set request attribute
ctx.attribute(key)           // get request attribute
ctx.attributeMap()           // get a map of request attributes

ctx.sessionAttribute(key)    // get request session attribute (from when WebSocket upgrade was performed)
ctx.sessionAttributeMap()    // get a map of session attributes (from when WebSocket upgrade was performed)

WsMessageContext

ctx.message() // String (String)
ctx.message(MyObject.class) // T (T)

WsBinaryMessageContext

ctx.data() // Byte[] (Array<Byte>)
ctx.offset() // int (Int)
ctx.length() // int (Int)

WsCloseContext

ctx.status() // int (Int)
ctx.reason() // String or null (String?)

WsErrorContext

ctx.error() // Throwable or null (Throwable?)

WsConnectContext

Doesn’t add anything to the base WsContext

Handler groups

You can group your endpoints by using the routes() and path() methods. routes() creates a temporary static instance of Javalin so you can skip the app. prefix before your handlers.

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

app.routes(() -> {
    path("users", () -> {
        get(UserController::getAllUsers);
        post(UserController::createUser);
        path(":id", () -> {
            get(UserController::getUser);
            patch(UserController::updateUser);
            delete(UserController::deleteUser);
        });
        ws("events", userController::webSocketEvents);
    });
});
app.routes {
    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 a routes() call:

app.routes(() -> {
    crud("users/:user-id", new UserController());
});
app.routes {
    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 access Javalin’s Validator class through the query parameter, path parameter, header, and body methods. Query parameters and form parameters can be given a default value for the case when a parameter is not present. Path parameters and headers cannot have default values.

// Query Parameters
// example url: /example?exampleId=123&color=blue&size=1&ts=1584647077000
String color = ctx.queryParam("color"); // blue
int exampleId = ctx.queryParam("exampleId", Integer.class).get(); // 123
int size = ctx.queryParam("size", Integer.class).check(i -> i > 4).get(); // exception
int qty = ctx.queryParam("qty", Integer.class, "12").get(); // uses default value 12
Instant instant = ctx.queryParam("ts", Instant.class).get();

// Path Parameters
// example url: /example/:exampleId/:name/:quantity/:timestamp-ms
String name = ctx.pathParam("name");
int exampleId = ctx.pathParam("exampleId", Integer.class).get();
int quantity = ctx.pathParam("quantity", Integer.class).check(i -> i > 4).get();
Instant instant = ctx.pathParam("timestamp-ms", Instant.class).get();

// Form Parameters
String color = ctx.formParam("color");
int exampleId = ctx.formParam("exampleId", Integer.class).get();
int size = ctx.formParam("size", Integer.class).check(i -> i > 4).get();
int qty = ctx.formParam("qty", Integer.class, "12").get(); // may default to value 12
Instant instant = ctx.queryParam("ts", Instant.class).get();

// Headers
String exampleHeaderStr = ctx.header("Example");
int version = ctx.header("Version", Integer.class).get();
int version = ctx.header("Version", Integer.class).check(i -> i > 4).get();
MyValue myValue = ctx.header("X-My-Header", MyValue.class).get();

// Body Validation
MyObject myObject = ctx.bodyValidator(MyObject.class).get();

// Query Parameters
// example url: /example?exampleId=123&color=blue&size=1&ts=1584647077000
val color = ctx.queryParam("color") // blue
val exampleId = ctx.queryParam<Int>("exampleId").get(); // 123
val size = ctx.queryParam<Int>("size").check({ it > 4 }).get(); // exception
val qty = ctx.queryParam<Int>("qty", "12").get(); // uses default value 12
val instant = ctx.queryParam<Instant>("ts").get();

// Path Parameters
// example url: /example/:exampleId/:name/:quantity/:timestamp-ms
val name = ctx.pathParam("name")
val exampleId = ctx.pathParam<Int>("exampleId").get()
val quantity = ctx.pathParam<Int>("quantity").check({ it > 4 }).get()
val instant = ctx.pathParam<Instant>("timestamp-ms").get()

// Form Parameters
val color = ctx.formParam("color");
val exampleId = ctx.formParam<Int>("exampleId").get();
val size = ctx.formParam<Int>("size").check(i -> i > 4).get();
val qty = ctx.formParam<Int>("qty", "12").get(); // may default to value 12
val instant = ctx.queryParam<Instant>("ts").get();

// Headers
val exampleHeaderStr = ctx.header("Example");
val version = ctx.header<Int>("Version").get();
val version = ctx.header<Int>("Version").check({ it > 4 }).get();
val myValue = ctx.header<MyValue>("X-My-Header").get();

// Body Validation
val myObject = ctx.bodyValidator<MyObject>();

Validator Nullability

If null is a valid value for your parameter, you can use getOrNull() instead of get().

Validator Error Collection

By default when accessing a value that has failed checks it will throw an exception on the first failed check. If you instead want to collect all failures (such as to return errors to be displayed on a form) you can access them with errors() which returns a map where the key is the value being checked (the param value in the case of headers, query params and path values) and the value is a list of error messages.

Validator<String> stringValidator = ctx.queryParam("first_name", String.class)
    .check(n -> !n.contains("-"), "cannot contain hyphens.")
    .check(n -> n.length() < 10, "cannot be longer than 10 characters.");

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

// Merges all errors from all validators in the list. Empty map if no errors exist.
Map<String, List<String>> manyErrors = Validator.collectErrors(stringValidator, otherValidator, etc)
val stringValidator = ctx.queryParam<String>("first_name")
    .check({ !it.contains("-") }, "cannot contain hyphens.")
    .check({ it.length < 10 }, "cannot be longer than 10 characters.")

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

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

Custom converters

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

JavalinValidation.register(Instant.class, v -> Instant.ofEpochMilli(v.toLong());
JavalinValidation.register(Instant::class.java) { Instant.ofEpochMilli(it.toLong()) }

Validation examples

// validate two dependent query parameters:
Instant fromDate = ctx.queryParam("from", Instant.class).get();
Instant toDate = ctx.queryParam("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)
        .get();
// validate two dependent query parameters:
val fromDate = ctx.queryParam<Instant>("from").get()
val toDate = ctx.queryParam<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 })
        .get()

If any of the validators find errors, a BadRequestResponse is thrown:

"Query parameter 'from' with value 'TEST' is not a valid Instant"
"Query parameter 'to' with value '1262347000000' invalid - 'to' has to be after 'from'"
"Request body as MyObject invalid - Check failed" // can set custom error message in check()

Access manager

Javalin has a functional interface AccessManager, which let’s you set per-endpoint authentication and/or authorization. It’s common to use before-handlers for this, but per-endpoint security handlers give you much more explicit and readable code. You can implement your access-manager however you want. Here is an example implementation:

// Set the access-manager that Javalin should use
config.accessManager((handler, ctx, permittedRoles) -> {
    MyRole userRole = getUserRole(ctx);
    if (permittedRoles.contains(userRole)) {
        handler.handle(ctx);
    } else {
        ctx.status(401).result("Unauthorized");
    }
});

Role getUserRole(Context ctx) {
    // determine user role based on request
    // typically done by inspecting headers
}

enum MyRole implements Role {
    ANYONE, ROLE_ONE, ROLE_TWO, ROLE_THREE;
}

app.routes(() -> {
    get("/un-secured",   ctx -> ctx.result("Hello"),   roles(ANYONE));
    get("/secured",      ctx -> ctx.result("Hello"),   roles(ROLE_ONE));
});
// Set the access-manager that Javalin should use
config.accessManager { handler, ctx, permittedRoles ->
    val userRole = getUserRole(ctx) // determine user role based on request
    if (permittedRoles.contains(userRole)) {
        handler.handle(ctx)
    } else {
        ctx.status(401).result("Unauthorized")
    }
}

fun getUserRole(ctx: Context) : Role {
    // determine user role based on request
    // typically done by inspecting headers
}

internal enum class MyRole : Role {
    ANYONE, ROLE_ONE, ROLE_TWO, ROLE_THREE
}

app.routes {
    get("/un-secured",   { ctx -> ctx.result("Hello")},   roles(MyRole.ANYONE));
    get("/secured",      { ctx -> ctx.result("Hello")},   roles(MyRole.ROLE_ONE));
}

The AccessManager will also run before your WebSocket upgrade request (if you have added roles to the endpoint), but keep in mind that WebSockets are long lived, so it might be wise to perform a check in wsBefore too/instead.

If you want to perform less restricted access management, you should consider using a before filter.

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) can throw Exception (and any subclass of Exception) The app.exception() method gives you a way of handling these 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
});
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 Exception Mapping

The different WebSocket handlers throw exceptions. The app.wsException() method gives you a way of handling these 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
});
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"));
});
app.sse("/sse") { client ->
    client.sendEvent("connected", "Hello, SSE")
    client.onClose { println("Client disconnected") }
}

The SseClient has access to three things:

client.sendEvent() // method(s) for sending events to client
client.onClose(runnable) // callback which runs when a client closes its connection
client.ctx // the Context for when the client connected (to fetch query-params, etc)

Configuration

You can pass a config object when creating a new instance of Javalin. The below snippets shows all the available config options:

Javalin.create(config -> {

    // JavalinServlet
    config.addSinglePageRoot(root, file)            // ex ("/", "/index.html")
    config.addSinglePageRoot(root, file, location)  // ex ("/", "src/file.html", Location.EXTERNAL)
    config.addStaticFiles(directory)                // ex ("/public")
    config.addStaticFiles(directory, location)      // ex ("src/folder", Location.EXTERNAL)
    config.addStaticFiles(prefix, dir, location)    // ex ("/assets", "src/folder", Location.EXTERNAL)
    config.aliasCheckForStaticFiles = AliasCheck    // symlink config, ex new ContextHandler.ApproveAliases();
    config.asyncRequestTimeout = timeoutInMs        // timeout for async requests (default is 0, no timeout)
    config.autogenerateEtags = true/false           // auto generate etags (default is false)
    config.compressionStrategy(Brotli(4), Gzip(6))  // set the compression strategy and levels - since 3.2.0
    config.contextPath = contextPath                // context path for the http servlet (default is "/")
    config.defaultContentType = contentType         // content type to use if no content type is set (default is "text/plain")
    config.dynamicGzip = true/false                 // dynamically gzip http responses (default is true)
    config.enableCorsForAllOrigins()                // enable cors for all origins
    config.enableCorsForOrigin(origins)             // enable cors for specific origins
    config.enableDevLogging()                       // enable extensive development logging for http and websocket
    config.enableWebjars()                          // enable webjars (static files)
    config.enforceSsl = true/false                  // redirect http traffic to https (default is false)
    config.ignoreTrailingSlashes = true/false       // default is true
    config.logIfServerNotStarted = true/false       // log a warning if user doesn't start javalin instance (default is true)
    config.precompressStaticFiles = true/false      // store compressed files in memory (avoid recompression and ensure content-length is set)
    config.prefer405over404 = true/false            // send a 405 if handlers exist for different verb on the same path (default is false)
    config.requestCacheSize = sizeInBytes           // set the request cache size, used for reading request body multiple times (default is 4kb)
    config.requestLogger { ... }                    // set a request logger
    config.sessionHandler { ... }                   // set a SessionHandler

    // WsServlet
    config.wsContextPath = contextPath              // context path for the websocket servlet (default is "/")
    config.wsFactoryConfig { ... }                  // set a websocket factory config
    config.wsLogger { ... }                         // set a websocket logger

    // Server
    config.server { ... }                           // set a Jetty server for Javalin to run on

    // Misc
    config.accessManager { ... }                    // set an access manager (affects both http and websockets)
    config.showJavalinBanner = true/false           // show the Javalin banner when starting the instance
}).start()
Javalin.create { config ->

    // JavalinServlet
    config.addSinglePageRoot(root, file)            // ex ("/", "/index.html")
    config.addSinglePageRoot(root, file, location)  // ex ("/", "src/file.html", Location.EXTERNAL)
    config.addStaticFiles(directory)                // ex ("/public")
    config.addStaticFiles(directory, location)      // ex ("src/folder", Location.EXTERNAL)
    config.addStaticFiles(prefix, dir, location)    // ex ("/assets", "src/folder", Location.EXTERNAL)
    config.aliasCheckForStaticFiles = AliasCheck    // symlink config, ex ContextHandler.ApproveAliases();
    config.asyncRequestTimeout = timeoutInMs        // timeout for async requests (default is 0, no timeout)
    config.autogenerateEtags = true/false           // auto generate etags (default is false)
    config.compressionStrategy(Brotli(4), Gzip(6))  // set the compression strategy and levels - since 3.2.0
    config.contextPath = contextPath                // context path for the http servlet (default is "/")
    config.defaultContentType = contentType         // content type to use if no content type is set (default is "text/plain")
    config.dynamicGzip = true/false                 // dynamically gzip http responses (default is true)
    config.enableCorsForAllOrigins()                // enable cors for all origins
    config.enableCorsForOrigin(origins)             // enable cors for specific origins
    config.enableDevLogging()                       // enable extensive development logging for http and websocket
    config.enableWebjars()                          // enable webjars (static files)
    config.enforceSsl = true/false                  // redirect http traffic to https (default is false)
    config.ignoreTrailingSlashes = true/false       // default is true
    config.logIfServerNotStarted = true/false       // log a warning if user doesn't start javalin instance (default is true)
    config.precompressStaticFiles = true/false      // store compressed files in memory (avoid recompression and ensure content-length is set)
    config.prefer405over404 = true/false            // send a 405 if handlers exist for different verb on the same path (default is false)
    config.requestCacheSize = sizeInBytes           // set the request cache size, used for reading request body multiple times (default is 4kb)
    config.requestLogger { ... }                    // set a request logger
    config.sessionHandler { ... }                   // set a SessionHandler

    // WsServlet
    config.wsContextPath = contextPath              // context path for the websocket servlet (default is "/")
    config.wsFactoryConfig { ... }                  // set a websocket factory config
    config.wsLogger { ... }                         // set a websocket logger

    // Server
    config.server { ... }                           // set a Jetty server for Javalin to run on

    // Misc
    config.accessManager { ... }                    // set an access manager (affects both http and websockets)
    config.showJavalinBanner = true/false           // show the Javalin banner when starting the instance
}.start()

Static Files

You can enabled static file serving by doing config.addStaticFiles("/classpath-folder"), and/or config.addStaticFiles("/folder", Location.EXTERNAL). Static resource handling is done after endpoint matching, meaning your self-defined endpoints have higher priority. The process looks like this:

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

If you do config.addStaticFiles("/classpath-folder"). Your index.html file at /classpath-folder/index.html will be available at http://{host}:{port}/index.html and http://{host}:{port}/.

You can call addStaticFiles multiple times to set up multiple handlers.

WebJars can be enabled by calling 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.

Path prefix

As of 3.9.0, you can call config.addStaticFiles("/hosting-path", "/dir-path"), which will make the files available on http://{host}:{port}/hosting-path/...

Caching

Javalin serves static files with the Cache-Control header set to max-age=0. This means that browsers will always ask if the file is still valid. If the version the browser has in cache is the same as the version on the server, Javalin will respond with a 304 Not modified status, and no response body. This tells the browser that it’s okay to keep using the cached version. If you want to skip this check, you can put files in a dir called immutable, and Javalin will set max-age=31622400, which means that the browser will wait one year before checking if the file is still valid. This should only be used for versioned library files, like vue-2.4.2.min.js, to avoid the browser ending up with an outdated version if you change the file content. WebJars also use max-age=31622400, as the version number is always part of the path.

Single page mode

Single page 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.addSinglePageRoot("/root", "/path/to/file.html"), and/or config.addSinglePageRoot("/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.addSinglePageHandler("/root",  ctx -> {
    ctx.html(...);
});

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>

Request logging

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

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

WebSocket logging

You can add a WebSocket logger by calling config.wsLogger(). The method takes a WsHandler, (the same interface 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:

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

The logger runs after the WebSocket handler for the endpoint.

Dev logging

app.create(config -> {
    config.enableDevLogging(); // enable extensive development logging for http and websocket
});
app.create { config ->
    config.enableDevLogging() // enable extensive development logging for http and websocket
}

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 server

If you need to customize the embedded server, you can call the server() method:

app.create(config -> {
    config.server(() -> {
        Server server = new Server(); // configure this however you want
        return server;
    }
});
app.create { config ->
    config.server {
        val server = Server() // configure this however you want
        server
    }
}

Custom SessionHandler

You can configure the SessionHandler by calling the sessionHandler(...) method.

If you want to persist sessions to the file system, you can use a FileSessionDataStore:

private fun fileSessionHandler() = SessionHandler().apply {
    httpOnly = true
    sessionCache = DefaultSessionCache(this).apply {
        sessionDataStore = FileSessionDataStore().apply {
            val baseDir = File(System.getProperty("java.io.tmpdir"))
            storeDir = File(baseDir, "javalin-session-store").apply { mkdir() }
        }
    }
}

Read more 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;
    })
}).start();
val statisticsHandler = StatisticsHandler()

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

SSL/HTTP2

To configure SSL or HTTP2 you need to use a custom server (see previous section).
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 3 introduced a new plugin system with two interfaces, Plugin and PluginLifecycleInit:

interface Plugin {
    void apply(@NotNull Javalin app);
}
interface PluginLifecycleInit {
    void init(@NotNull Javalin app);
}

When implementing PluginLifecycleInit#init, you are not allowed to add Handler instances to the app.
The two interface methods are called like this during setup:

initPlugins.forEach(plugin -> {
    plugin.init(app);
    // will throw exception if `init` adds Handler
});

plugins.forEach(plugin -> plugin.apply(app));

This is mainly so each plugin has a chance to add handlerAdded listeners before other plugins add their handlers, so that each plugin has a complete overview of all handlers.

Route overview plugin

You can enable a HTML page showing all the routes of your application by registering it on the config:

Javalin.create(config ->
    config.registerPlugin(new RouteOverviewPlugin(path));        // show all routes on specified path
    config.registerPlugin(new RouteOverviewPlugin(path, roles)); // show all routes on specified path (with auth)
)

Micrometer Plugin

You can enable the Micrometer plugin by registering it on the config:

Javalin.create(config ->
    config.registerPlugin(new MicrometerPlugin());
)

Additional documentation for the plugin can be found here.

OpenAPI Plugin

Javalin has an OpenAPI (Swagger) plugin. Full documentation for the plugin can be found here, below are a few examples:

OpenAPI DSL

When using the OpenAPI DSL you define an OpenApiDocumentation object to pair with your Handler:

val addUserDocs = document()
        .body<User>()
        .result<Unit>("400")
        .result<Unit>("204")

fun addUserHandler(ctx: Context) {
    val user = ctx.body<User>()
    UserRepository.addUser(user)
    ctx.status(204)
}

You then combine these when you add your routes:

post("/users", documented(addUserDocs, ::addUserHandler))

OpenAPI annotations

If you prefer to keep your documentation separate from your code, you can use annotations instead:

@OpenApi(
    requestBody = OpenApiRequestBody(User::class),
    responses = [
        OpenApiResponse("400", Unit::class),
        OpenApiResponse("201", Unit::class)
    ]
)
fun createUser(ctx: Context) {
    val user = ctx.body<User>()
    UserRepository.createUser(user)
    ctx.status(201)
}

If you use the annotation API you don’t need to connect the documentation and the handler manually, you just reference your handler as normal:

post("/users", ::addUserHandler)

Javalin will then extract the information from the annotation and build the documentation automatically.

To enable hosted docs you have to specify some paths in your Javalin config:

val app = Javalin.create {
    it.enableOpenApi(
            OpenApiOptions(Info().version("1.0").description("My Application"))
                    .path("/swagger-json")
                    .swagger(SwaggerOptions("/swagger").title("My Swagger Documentation"))
                    .reDoc(ReDocOptions("/redoc").title("My ReDoc Documentation"))
    )
}

Full documentation for the OpenAPI plugin can be found at /plugins/openapi.

GraphQL plugin

Javalin has an GraphQL plugin. You can see its documentation at /plugins/graphql.

Redirect-to-lowercase-path plugin

This plugin redirects requests with uppercase/mixcase paths to lowercase paths. For example, /Users/John redirects to /users/John (if endpoint is /users/:userId). It does not affect the casing of path-params and query-params, only static URL fragments (Users becomes users above, but John remains John).
When using this plugin, you can only add paths with lowercase URL fragments.

Javalin.create(config ->
    config.registerPlugin(new RedirectToLowercasePathPlugin());
)

Rate limiting

There is a very simple rate-limited included in Javalin 3.7.0 and newer. You can call it in the beginning of your endpoint Handler functions:

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

Every rate limiter is independent (IP and Handler based), so different endpoints can have different rate limits. It works as follows:

Modules

As of 3.9.0, Javalin is a multi-module project. The current modules (for 6.1.3) are:

FAQ

Frequently asked questions.

Android

To use Javalin in an Android project, you will need to:

1: Target the Android SDK 26 and higher:

defaultconfig {
  minSdkVersion 26
  targetSdkVersion 28
}

2: Target Java 8:

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

3: Insert this in your build.gradle file:

packagingOptions {
  exclude 'org/eclipse/jetty/http/encoding.properties'
}

4: Specify android.enableD8=true in your gradle.properties file.


Concurrency

By default, Javalin serves requests using a Jetty QueuedThreadPool with 250 threads. Handlers are invoked in parallel on multiple threads, so all handler implementations should be thread-safe.

The default configuration adds a very thin abstraction layer on top of Jetty. It has similar performance to raw Jetty, which is able to handle over a million plaintext requests per second.

If you have a lot of long running requests, it might be worth looking into Asynchronous requests, or setting up Javalin with project Loom.

If you’re not sure if you need async requests, you probably don’t.

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. There is also a tutorial on deploying Javalin to Heroku.


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 the Javalin.createStandalone() factory method.

This method will create a Javalin instance, which exposes the HttpServlet that Javalin uses to handle HTTP requests (via app.servlet()). Please note that Javalin’s WebSockets functionality has a hard dependency on Jetty, and will not work in standalone mode.

Remember to exclude Jetty when setting this up. If you need more instructions, follow the tutorial.


Uploads

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

app.post("/upload", ctx -> {
    ctx.uploadedFiles("files").forEach(uploadedFile -> {
        FileUtil.streamToFile(uploadedFile.getContent(), "upload/" + uploadedFile.getFilename())
    });
});
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

While the default threadpool (200 threads) is enough for most use cases, sometimes slow operations should be run asynchronously. Luckily it’s very easy in Javalin, just pass a CompletableFuture to ctx.result():

import io.javalin.Javalin

fun main(args: Array<String>) {
    val app = Javalin.create().start(7000)
    app.get("/") { ctx -> ctx.result(getFuture()) }
}

// hopefully your future is less pointless than this:
private fun getFuture() = CompletableFuture<String>().apply {
    Executors.newSingleThreadScheduledExecutor().schedule({ this.complete("Hello World!") }, 1, TimeUnit.SECONDS)
}
Synonyms for ctrl+f: Async, CompletableFuture, Future, Concurrent, Concurrency

You can only set future results in endpoint handlers (get/post/put/etc).
After-handlers, exception-handlers and error-handlers run like you’d expect them to after the future has been resolved or rejected.

Async timeout settings

Jetty has a default timeout of 30 seconds for async requests (this is not related to the idleTimeout of a connector). If you wait for processes that run for longer than this, you can configure the async request manually by calling ctx.req.startAsync(). For more information, see issue 448.


Configuring the JSON mapper

Note that these are global settings, and can’t be configured per instance of Javalin.

Configuring Jackson

The JSON mapper uses Jackson by default, which can be configured by calling:

JavalinJackson.configure(objectMapper)

Using Gson

Javalin can be configured to use Gson instead of Jackson. In Java:

Gson gson = new GsonBuilder().create();
JavalinJson.setFromJsonMapper(gson::fromJson);
JavalinJson.setToJsonMapper(gson::toJson);

In Kotlin:

val gson = GsonBuilder().create()

JavalinJson.fromJsonMapper = object : FromJsonMapper {
    override fun <T> map(json: String, targetClass: Class<T>) = gson.fromJson(json, targetClass)
}

JavalinJson.toJsonMapper = object : ToJsonMapper {
    override fun map(obj: Any): String = gson.toJson(obj)
}

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


Views and Templates

Javalin looks for templates/markdown files in src/resources, and uses the correct rendering engine based on the extension of your template. Javalin currently supports six template engines (see below), as well as markdown. You can also register your own rendering engine.

ctx.render("/templateFile.ext", model("firstName", "John", "lastName", "Doe"))
ctx.render("/templateFile.ext", mapOf("firstName" to "John", "lastName" to "Doe"))

Register:

JavalinRenderer.register(JavalinPebble.INSTANCE, ".peb", ".pebble");

JavalinRenderer.register((filePath, model) -> {
    return MyRenderer.render(filePath, model);
}, ".ext");

Configure:

JavalinThymeleaf.configure(templateEngine)
JavalinVelocity.configure(velocityEngine)
JavalinFreemarker.configure(configuration)
JavalinMustache.configure(mustacheFactory)
JavalinJtwig.configure(configuration)
JavalinPebble.configure(configuration)
JavalinCommonmark.configure(htmlRenderer, markdownParser)

Note that these are global settings, and can’t be configured per instance of Javalin.


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 and query-parameters in the Vue instance, which you can access:

<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’s a tutorial explaining the concepts: /tutorials/simple-frontends-with-javalin-and-vue

Shared state

If you want to share state from your server with Vue, you can provide JavalinVue with a state function:

JavalinVue.stateFunction = { ctx -> mapOf("user" to getUser(ctx)) }

This can then be accessed from the state variable:

<template id="user-template">
    <div>{{ $javalin.state.user }}</div>
</template>

The function runs for every request, so the state is always up to date when the user navigates or refreshes the page.

Inline files

You can inline files into your layout template by using the following functions:

<head>
    <style>@inlineFile("/vue/styles.css")</style> <!-- always included -->
    <script>@inlineFileDev("/vue/scripts-dev.js")</script> <!-- only included in dev -->
    <script>@inlineFileNotDev("/vue/scripts-not-dev.js")</script> <!-- only included in not dev -->
</head>

CDN WebJars

You can reference your WebJars with @cdnWebjar/ instead of the normal /webjars/. If you do this, the path will resolve to /webjars/ on when isDevFunction returns true, and https//cdn.jsdelivr.net/.../ on non-localhost. Note that this only works with NPM webjars.

Vue directory location

By default, JavalinVue will set the vue root directory based on the first request it serves.

This can cause issues when running a jar locally or in docker. You can override the default dir:

JavalinVue.rootDirectory(path, location); // String path, String location
JavalinVue.rootDirectory(path); // java.nio.Path path

isDevFunction

You can override the JavalinVue.isDevFunction to let JavalinVue know if the environment is develop or not. This is used to disable caching on dev to speed up development. The default function returns true if the request is on localhost.

Optimize dependencies

If you set JavalinVue.optimizeDependencies to true, JavalinVue will only load the required dependencies for your route component. This is set to false by default.


TimeoutExceptions and ClosedChannelExceptions

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.

Documentation for previous versions

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.

Like Javalin?
Star us 😊

×