∞ do more, more easily

1. Introduction

Jooby is a modern, performant and easy to use web framework for Java and Kotlin built on top of your favorite web server.

Welcome!!
Java
Kotlin
import io.jooby.Jooby;

public class App extends Jooby {

  {
    get("/", ctx -> "Welcome to Jooby!");
  }

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}

Latest Release: 3.5.0.

Looking for a previous version?

1.1. Features

1.2. Script API

Script API (a.k.a. script routes) provides a fluent DSL based on lambda functions, free of reflection and annotations.

We usually extend Jooby and define routes in the instance initializer:

Script with sub-class:
Java
Kotlin
import io.jooby.Jooby;

public class App extends Jooby {

  {
    get("/", ctx -> "Hello Jooby!");
  }

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}

For Java applications we favor extending Jooby because the DSL looks better (no need to prefix the get method with a variable).

This is not strictly necessary (of course); you may prefer to do it without extending Jooby:

Script without subclass:
Java
Kotlin
import io.jooby.Jooby;

public class App {

  public static void main(String[] args) {
    runApp(args, app -> {

      app.get("/", ctx -> "Hello Jooby!");

    });
  }
}

For Kotlin, it doesn’t matter which one you choose. The DSL looks great with or without extending Kooby.

1.3. MVC API

The MVC API (a.k.a. MVC routes) uses annotation to define routes and byte code generation to execute them.

MVC API:
Java
Kotlin
import io.jooby.annotation.*;

public class MyController {

  @GET
  public String sayHi() {
    return "Hello Jooby!";
  }
}

public class App {

  public static void main(String[] args) {
    runApp(args, app -> {

      app.use(new MyController());

    });
  }
}

Read more about MVC and JAX-RS support in the MVC API chapter.

2. Getting Started

The best way of getting started is using the jooby console. It is a small application that generates Jooby projects very quickly.

Features

  • Maven or Gradle build

  • Java or Kotlin application

  • Script or MVC routes

  • Jetty, Netty or Undertow application

  • Uber/Fat jar or Stork native launcher

  • Dockerfile

To install the console:

  • Download jooby-cli.zip

  • Unzip jooby-cli.zip in your user home directory (or any other directory you prefer)

  • Find the native launchers in the bin directory

You might want to add the native launcher bin/jooby or bin/jooby.bat to your system path variable, to make it globally accessible from any location.

To simplify documentation we use jooby as command, it requires Java 17 as minimum. Windows users must use jooby.bat

Setting workspace:
jooby set -w ~/Source

All code will be saved inside the ~/Source directory.

Workspace directory is ready!

Now type jooby and hit ENTER.

After prompt, type help create:

jooby
jooby> help create
Missing required parameter: <name>
Usage: jooby create [-dgikms] [--server=<server>] <name>
Creates a new application
      <name>              Application name or coordinates (groupId:artifactId:
                            version)
  -d, --docker            Generates a Dockerfile
  -g, --gradle            Generates a Gradle project
  -i                      Start interactive mode
  -k, --kotlin            Generates a Kotlin application
  -m, --mvc               Generates a MVC application
  -s, --stork             Add Stork Maven plugin to build (Maven only)
      --server=<server>   Choose one of the available servers: jetty, netty or
                            undertow
jooby>

The create command generates a Jooby application. Some examples:

Creates a Maven Java project:
jooby> create myapp
Creates a Maven Kotlin project:
jooby> create myapp --kotlin
Since 3.x

Kotlin was removed from core, you need to the jooby-kotlin dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-kotlin</artifactId>
  <version>3.5.0</version>
</dependency>
Creates a Gradle Java project:
jooby> create myapp --gradle
Creates a Gradle Kotlin project:
jooby> create myapp --gradle --kotlin

Maven and Java are the default options but you can easily override these with -g -k or -gk (order doesn’t matter). Along with the build and language, the create command adds two test classes: UnitTest and IntegrationTest.

Passing the -m or --mvc generates an MVC application:

Creates a Maven Java Mvc project:
jooby> create myapp --mvc

The --server option, allow you to choose between: (J)etty, (N)etty or (U)ndertow:

Creates a Maven Java Project using Undertow:
jooby> create myapp --server undertow

Maven/Gradle configuration generates an uber/fat jar at package time. Maven builds supports generation of Stork launchers.

Creates a Maven Java Project with stork launchers:
jooby> create myapp --stork

There is a -d or --docker option which generates a Dockerfile

Creates a docker file:
jooby> create myapp --docker

The default package in all these examples is set to app. To get full control of groupId, package, version, etc., use the interactive mode:

Interactive mode:
jooby> create myapp -i

2.1. Code Snippets

For simplicity and brevity we are going to skip the runApp function and extending Jooby. Code example will look like:

Snippet
Java
Kotlin
{
  get("/", ctx -> "Snippet");
}

The use of application class or runApp function will be included when strictly necessary.

3. Router

The Router is the heart of Jooby and consist of:

  • Routing algorithm (radix tree)

  • One or more routes

  • Collection of operator over routes

3.1. Route

A Route consists of three part:

Routes:
Java
Kotlin
{

  (1) (2)
  get("/foo", ctx -> {
    return "foo"; (3)
  });

  // Get example with path variable
  get("/foo/{id}", ctx -> {
    return ctx.path("id").value();
  });

  // Post example
  post("/", ctx -> {
    return ctx.body().value();
  });
}
1 HTTP method/verb, like: GET, POST, etc…​
2 Path pattern, like: /foo, /foo/{id}, etc…​
3 Handler function

The handler function always produces a result, which is send it back to the client.

3.1.1. Attributes

Attributes let you annotate a route at application bootstrap time. It functions like static metadata available at runtime:

Java
Kotlin
{
  get("/foo", ctx -> "Foo")
    .attribute("foo", "bar");
}

An attribute consist of a name and value. Values can be any object. Attributes can be accessed at runtime in a request/response cycle. For example, a security module might check for a role attribute.

Java
Kotlin
{
  use(next -> ctx -> {
    User user = ...;
    String role = ctx.getRoute().attribute("Role");

    if (user.hasRole(role)) {
        return next.apply(ctx);
    }

    throw new StatusCodeException(StatusCode.FORBIDDEN);
  });
}

In MVC routes you can set attributes via annotations:

Java
Kotlin
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Role {
  String value();
}

@Path("/path")
public class AdminResource {

  @Role("admin")
  public Object doSomething() {
    ...
  }

}

{
  use(next -> ctx -> {
    System.out.println(ctx.getRoute().attribute("Role"));
  });
}

The previous example will print: admin. You can retrieve all the attributes of the route by calling ctx.getRoute().getAttributes().

Any runtime annotation is automatically added as route attributes following these rules: - If the annotation has a value method, then we use the annotation’s name as the attribute name. - Otherwise, we use the method name as the attribute name.

3.2. Path Pattern

3.2.1. Static

Java
Koltin
{
  get("/foo", ctx -> "Foo");
}

3.2.2. Variable

Single path variable:
Java
Kotlin
{
  (1)
  get("/user/{id}", ctx -> {
    int id = ctx.path("id").intValue(); (2)
    return id;
  });
}
1 Defines a path variable id
2 Retrieve the variable id as int
Multiple path variables:
Java
Kotlin
{
  (1)
  get("/file/{file}.{ext}", ctx -> {
    String filename = ctx.path("file").value(); (2)
    String ext = ctx.path("ext").value();   (3)
    return filename + "." + ext;
  });
}
1 Defines two path variables: file and ext
2 Retrieve string variable: file
3 Retrieve string variable: ext
Optional path variable:
Java
Kotlin
{
  (1)
  get("/profile/{id}?", ctx -> {
    String id = ctx.path("id").value("self"); (2)
    return id;
  });
}
1 Defines an optional path variable id. The trailing ? make it optional.
2 Retrieve the variable id as String when present or use a default value: self.

The trailing ? makes the path variable optional. The route matches:

  • /profile

  • /profile/eespina

3.2.3. Regex

Regex path variable:
Java
Kotlin
{
  (1)
  get("/user/{id:[0-9]+}", ctx -> {
    int id = ctx.path("id").intValue(); (2)
    return id;
  });
}
1 Defines a path variable: id. Regex expression is everything after the first :, like: [0-9]+
2 Retrieve an int value

Optional syntax is also supported for regex path variable: /user/{id:[0-9]+}?:

  • matches /user

  • matches /user/123

3.2.4. * Catchall

catchall
Java
Kotlin
{
  (1)
  get("/articles/*", ctx -> {
    String catchall = ctx.path("*").value(); (2)
    return catchall;
  });

  get("/articles/*path", ctx -> {
    String path = ctx.path("path").value(); (3)
    return path;
  });
}
1 The trailing * defines a catchall pattern
2 We access to the catchall value using the * character
3 Same example, but this time we named the catchall pattern and we access to it using path variable name.

A catchall pattern must be defined at the end of the path pattern.

3.3. Handler

Application logic goes inside a handler. A handler is a function that accepts a context object and produces a result.

A context allows you to interact with the HTTP Request and manipulate the HTTP Response.

Incoming request matches exactly ONE route handler. If there is no handler, produces a 404 response.

Java
Kotlin
{
  get("/user/{id}", ctx -> ctx.path("id").value());  (1)

  get("/user/me", ctx -> "my profile");              (2)

  get("/users", ctx -> "users");                     (3)

  get("/users", ctx -> "new users");                 (4)
}

Output:

1 GET /user/ppicapiedrappicapiedra
2 GET /user/memy profile
3 Unreachable ⇒ override it by next route
4 GET /usersnew users not users

Routes with most specific path pattern (2 vs 1) has more precedence. Also, is one or more routes result in the same path pattern, like 3 and 4, last registered route hides/overrides previous route.

3.3.1. Filter

Cross cutting concerns such as response modification, verification, security, tracing, etc. is available via Route.Filter.

A filter takes the next handler in the pipeline and produces a new handler:

interface Filter {
  Handler apply(Handler next);
}
Timing filter example:
Java
Kotlin
{
  use(next -> ctx -> {
    long start = System.currentTimeMillis();       (1)

    Object response = next.apply(ctx);             (2)

    long end = System.currentTimeMillis();
    long took = end - start;

    System.out.println("Took: " + took + "ms");   (3)

    return response;                              (4)
  });

  get("/", ctx -> {
    return "filter";
  });
}
1 Saves start time
2 Proceed with execution (pipeline)
3 Compute and print latency
4 Returns a response

One or more filter on top of a handler produces a new handler.

3.3.2. Before

The before filter runs before a handler.

A before filter takes a context as argument and don’t produces a response. It expected to operates via side effects (usually modifying the HTTP response).

interface Before {
  void apply(Context ctx);
}
Example
Java
Kotlin
{
  before(ctx -> {
    ctx.setResponseHeader("Server", "Jooby");
  });

  get("/", ctx -> {
    return "...";
  });
}

3.3.3. After

The after filter runs after a handler.

An after filter takes three arguments. The first argument is the HTTP context, the second argument is the result/response from a functional handler or null for side-effects handler, the third and last argument is an exception generates from handler.

It expected to operates via side effects, usually modifying the HTTP response (if possible) or for cleaning/trace execution.

interface After {
  void apply(Context ctx, Object result, Throwable failure);
}
Functional Handler:
Java
Kotlin
{
  after((ctx, result, failure) -> {
    System.out.println(result);          (1)
    ctx.setResponseHeader("foo", "bar"); (2)
  });

  get("/", ctx -> {
    return "Jooby";
  });
}
1 Prints Jooby
2 Add a response header (modifies the HTTP response)

If the target handler is a functional handler modification of HTTP response is allowed it.

For side effects handler the after filter is invoked with a null value and isn’t allowed to modify the HTTP response.

Side-Effect Handler:
Java
Kotlin
{
  after((ctx, result, failure) -> {
    System.out.println(result);          (1)
    ctx.setResponseHeader("foo", "bar"); (2)
  });

  get("/", ctx -> {
    return ctx.send("Jooby");
  });
}
1 Prints null (no value)
2 Produces an error/exception

Exception occurs because response was already started and its impossible to alter/modify it.

Side-effects handler are all that make use of family of send methods, responseOutputStream and responseWriter.

You can check whenever you can modify the response by checking the state of isResponseStarted():

Safe After:
Java
Kotlin
{
  after((ctx, result, failure) -> {
    if (ctx.isResponseStarted()) {
      // Don't modify response
    } else {
      // Safe to modify response
    }
  });
}

An after handler is always invoked.

The next examples demonstrate some use cases for dealing with errored responses, but keep in mind that an after handler is not a mechanism for handling and reporting exceptions that’s is a task for an Error Handler.

Run code depending of success or failure responses:
Java
Kotlin
{
  after((ctx, result, failure) -> {
    if (failure == null) {
      db.commit();                   (1)
    } else {
      db.rollback();                 (2)
    }
  });
}

Here the exception is still propagated given the chance to the Error Handler to jump in.

Recover fom exception and produces an alternative output:
Java
Kotlin
{
  after((ctx, result, failure) -> {
    if (failure instanceOf MyBusinessException) {
      ctx.send("Recovering from something");        (1)
    }
  });
}
1 Recover and produces an alternative output

Here the exception wont be propagated due we produces a response, so error handler won’t be execute it.

In case where the after handler produces a new exception, that exception will be add to the original exception as suppressed exception.

Suppressed exceptions:
Java
Kotlin
{
  after((ctx, result, failure) -> {
    ...
    throw new AnotherException();
  });

  get("/", ctx -> {
    ...
    throw new OriginalException();
  });

  error((ctx, failure, code) -> {
    Throwable originalException = failure;                       (1)
    Throwable anotherException  = failure.getSuppressed()[0];    (2)
  });
}
1 Will be OriginalException
2 Will be AnotherException

3.3.4. Complete

The complete listener run at the completion of a request/response cycle (i.e. when the request has been completely read, and the response has been fully written).

At this point it is too late to modify the exchange further. They are attached to a running context (not like a filter/before/after filters).

Example
Java
Kotlin
{
   use(next -> ctx -> {
     long start = System.currentTimeInMillis();
     ctx.onComplete(context -> {                      (1)
       long end = System.currentTimeInMillis();       (2)
       System.out.println("Took: " + (end - start));
     });
   });
}
1 Attach a completion listener
2 Run after response has been fully written

Completion listeners are invoked in reverse order.

3.4. Pipeline

Route pipeline (a.k.a route stack) is a composition of one or more use(s) tied to a single handler:

Java
Kotlin
{
  // Increment +1
  use(next -> ctx -> {
    Number n = (Number) next.apply(ctx);
    return 1 + n.intValue();
  });

  // Increment +1
  use(next -> ctx -> {
    Number n = (Number) next.apply(ctx);
    return 1 + n.intValue();
  });

  get("/1", ctx -> 1); (1)

  get("/2", ctx -> 2); (2)
}

Output:

1 /13
2 /24

Behind the scene, Jooby builds something like:

{
  // Increment +1
  var increment = use(next -> ctx -> {
    Number n = (Number) next.apply(ctx);
    return 1 + n.intValue();
  });

  Handler one = ctx -> 1;

  Handler two = ctx -> 2;

  Handler handler1 = increment.then(increment).then(one);
  Handler handler2 = increment.then(increment).then(two);

  get("/1", handler1);

  get("/2", handler2);
}

Any filter defined on top of the handler will be stacked/chained into a new handler.

Filter without path pattern

This was a hard decision to make, but we know is the right one. Jooby 1.x uses a path pattern to define filter.

The pipeline in Jooby 1.x consists of multiple filters and handlers. They are match sequentially one by one. The following filter is always executed in Jooby 1.x

Jooby 1.x
{
   use("/*", (req, rsp, chain) -> {
     // remote call, db call
   });

   // ...
}

Suppose there is a bot trying to access and causing lot of 404 responses (path doesn’t exist). In Jooby 1.x the filter is executed for every single request sent by the bot just to realize there is NO matching route and all we need is a 404.

In Jooby 2.x this won’t happen anymore. If there is a matching handler, the pipeline will be executed. Otherwise, nothing will do!

3.4.1. Order

Order follows the what you see is what you get approach. Routes are stacked in the way they were added/defined.

Order example:
Java
Kotlin
{
  // Increment +1
  use(next -> ctx -> {
    Number n = (Number) next.apply(ctx);
    return 1 + n.intValue();
  });

  get("/1", ctx -> 1);                (1)

  // Increment +1
  use(next -> ctx -> {
    Number n = (Number) next.apply(ctx);
    return 1 + n.intValue();
  });

  get("/2", ctx -> 2);               (2)
}

Output:

1 /12
2 /24

3.4.2. Scoped Filter

The route(Runnable) and path(String,Runnable) operators are used to group one or more routes.

A scoped filter looks like:

Scoped filter:
Java
Kotlin
{
  // Increment +1
  use(next -> ctx -> {
    Number n = (Number) next.apply(ctx);
    return 1 + n.intValue();
  });

  routes(() -> {                          (1)
    // Multiply by 2
    use(next -> ctx -> {
      Number n = (Number) next.apply(ctx);
      return 2 * n.intValue();
    });

    get("/4", ctx -> 4);                 (2)
  });

  get("/1", ctx -> 1);                   (3)
}

Output:

1 Introduce a new scope via route operator
2 /49
3 /12

It is a normal filter inside of one of the group operators.

3.5. Grouping routes

As showed previously, the route(Runnable) operator push a new route scope and allows you to selectively apply one or more routes.

Route operator
Java
Kotlin
{
  routes(() -> {

    get("/", ctx -> "Hello");

  });
}

Route operator is for grouping one or more routes and apply cross cutting concerns to all them.

In similar fashion the path(String,Runnable) operator groups one or more routes under a common path pattern.

Routes with path prefix:
Java
Kotlin
{
   path("/api/user", () -> {    (1)

     get("/{id}", ctx -> ...);  (2)

     get("/", ctx -> ...);      (3)

     post("/", ctx -> ...);     (4)

     ...
   });
}
1 Set common prefix /api/user
2 GET /api/user/{id}
3 GET /api/user
4 POST /api/user

3.6. Composing

3.6.1. Mount

Composition is a technique for building modular applications. You can compose one or more router into a new one.

Composition is available through the mount(Router) operator:

Composing
Java
Kotlin
public class Foo extends Jooby {
  {
    get("/foo", Context::getRequestPath);
  }
}

public class Bar extends Jooby {
  {
    get("/bar", Context::getRequestPath);
  }
}

public class App extends Jooby {
  {
    mount(new Foo());                       (1)

    mount(new Bar());                       (2)

    get("/app", Context::getRequestPath);   (3)
  }
}
1 Imports all routes from Foo. Output: /foo/foo
2 Imports all routes from Bar. Output: /bar/bar
3 Add more routes . Output /app/app
Composing with path prefix
Java
Kotlin
public class Foo extends Jooby {
  {
    get("/foo", Context::getRequestPath);
  }
}

public class App extends Jooby {
  {
    mount("/prefix", new Foo());  (1)
  }
}
1 Now all routes from Foo will be prefixed with /prefix. Output: /prefix/foo/prefix/foo

The mount operator only import routes. Services, callbacks, etc…​ are not imported. Main application is responsible for assembly all the resources and services required by imported applications.

3.6.2. Install

Alternatively, you can install a standalone application into another one using the install(Jooby) operator:

Installing
Java
Kotlin
public class Foo extends Jooby {
  {
    get("/foo", ctx -> ...);
  }
}

public class Bar extends Jooby {
  {
    get("/bar", ctx -> ...);
  }
}

public class App extends Jooby {
  {
    install(Foo::new);                       (1)

    install(Bar::new);                       (2)
  }
}
1 Imports all routes, services, callbacks, etc…​ from Foo. Output: /foo/foo
2 Imports all routes, services, callbacks, etc…​ from Bar. Output: /bar/bar

This operator lets you for example to deploy Foo as a standalone application or integrate it into a main one called App3508. The install operator shares the state of the main application, so lazy initialization (and therefore instantiation) of any child applications is mandatory.

For example, this won’t work:

Java
{
  Foo foo = new Foo();
  install(() -> foo);        // Won't work
}

The Foo application must be lazy initialized:

Java
{
  install(() -> new Foo());  // Works!
}

3.7. Dynamic Routing

Dynamic routing looks similar to composition but enables/disables routes at runtime using a predicate.

Suppose you own two versions of an API and for some time you need to support both the old and new APIs:

Dynamic Routing
Java
Kotlin
public class V1 extends Jooby {
  {
    get("/api", ctx -> "v1");
  }
}

public class V2 extends Jooby {
  {
    get("/api", ctx -> "v2");
  }
}

public class App extends Jooby {
  {
    mount(ctx -> ctx.header("version").value().equals("v1"), new V1()); (1)

    mount(ctx -> ctx.header("version").value().equals("v2"), new V2()); (2)
  }
}

Output:

1 /apiv1; when version header is v1
2 /apiv2; when version header is v2

Done ♡!

3.8. Options

3.8.1. Hidden Method

The setHiddenMethod(String) option allow clients to override the HTTP method using a hidden form field.

Client
<form method="post" action="/form">
  <input type="hidden" name="_method" value="put">
</form>
Server
Java
Kotlin
import io.jooby.Jooby;
...
{

  setHiddenMethod("_method");                         (1)

  put("/form", ctx -> {                               (2)

  });
}
1 Configure hidden method property to read the form field: _method
2 Execute the put version of /form

The default implementation looks for a form field from POST form/multipart request. You can provide a different strategy.

HTTP Header
Java
Kotlin
import io.jooby.Jooby;
...
{

  setHiddenMethod(ctx -> ctx.header("X-HTTP-Method-Override").toOptional());  (1)
}
1 Look for a request header: X-HTTP-Method-Override

3.8.2. Trust Proxy

The setTrustProxy(boolean) option enables parsing of X-Forwarded-* headers.

Usage
Java
Kotlin
import io.jooby.Jooby;
...
{

  setTrustProxy(true)                                 (1)

  get("/", ctx -> {
    String remoteAddress = ctx.getRemoteAddress();    (2)
    String scheme = ctx.getScheme();                  (3)
    String host = ctx.getHost();                      (4)
    int port = ctx.getPort();                         (5)
    ...
  });
}
1 Set trust proxy
2 Set remote address from X-Forwarded-For
3 Set scheme from X-Forwarded-Proto
4 Set host from X-Forwarded-Host
5 Set port from X-Forwarded-Host or X-Forwarded-Port

This should only be installed behind a reverse proxy that has been configured to send the X-Forwarded-* header, otherwise a remote user can spoof their address by sending a header with bogus values.

4. Context

A Context allows you to interact with the HTTP Request and manipulate the HTTP Response.

In most of the cases you can access the context object as a parameter of your route handler:

Java
Kotlin
{
  get("/", ctx -> { /* do important stuff with variable 'ctx'. */ });
}

If you need to access the context via the service registry or dependency injection, you need to explicitly request the registration of it as a service with the following invocation:

Java
Kotlin
{
  setContextAsService(true);
}

Important to note that the context is a request scoped object, it’s only available through the service registry while the request it belongs to is being processed.

Context also provides derived information about the current request such as a matching locale (or locales) based on the Accept-Language header (if presents). You may use the result of locale() or locales() to present content matching to the user’s language preference.

The above methods use Locale.lookup(…​) and Locale.filter(…​) respectively to perform the language tag matching. See their overloads if you need to plug in your own matching strategy.

To leverage language matching however, you need to tell Jooby which languages your application supports. This can be done by either setting the application.lang configuration property to a value compatible with the Accept-Language header:

application.conf
application.lang = en, en-GB, de

or calling the setLocales(List) or setLocales(Locale…​) method at runtime:

Java
Kotlin
{
  setLocales(Locale.GERMAN, new Locale("hu", "HU"));
}

If you don’t set the supported locales explicitly, Jooby uses a single locale provided by Locale.getDefault().

4.1. Parameters

There are several parameter types: header, cookie, path, query, form, multipart, session and flash. All them share a unified/type-safe API for accessing and manipulating their values.

We are going to describe them briefly in the next sections, then go into specific features of the Value API.

There is also a parameter lookup feature by which you can access a parameter from any combination of the above types with well-defined priority.

4.1.1. Header

HTTP headers allow the client and the server to pass additional information with the request or the response.

Java
Kotlin
{
  get("/", ctx -> {
    String token = ctx.header("token").value();      (1)

    Value headers = ctx.headers();                   (2)

    Map<String, String> headerMap = ctx.headerMap(); (3)
    ...
  });

}
1 Header variable token
2 All headers as Value
3 All headers as map

Request cookies are send to the server using the Cookie header, but we do provide a simple key/value access to them:

Cookies
Java
Kotlin
{
  get("/", ctx -> {
    String token = ctx.cookie("token").value();      (1)

    Map<String, String> cookieMap = ctx.cookieMap(); (2)
    ...
  });

}
1 Cookie variable token
2 All cookies as map

4.1.3. Path

Path parameter are part of the URI. To define a path variable you need to use the {identifier} notation.

Syntax:
Java
Kotlin
{
  get("/{id}" ctx -> ctx.path("id").value());                                 (1)

  get("/@{id}" ctx -> ctx.path("id").value());                                (2)

  get("/file/{name}.{ext}", ctx -> cxt.path("name") + "." + ctx.path("ext")); (3)

  get("/file/*", ctx -> ctx.path("*"))                                        (4)

  get("/{id:[0-9]+}", ctx -> ctx.path("id))                                   (5)
}
1 Path variable id
2 Path variable id prefixed with @
3 Multiple variables name and ext
4 Unnamed catchall path variable
5 Path variable with a regular expression
Java
Kotlin
{
  get("/{name}", ctx -> {
    String pathString = ctx.getRequestPath();     (1)

    Value path = ctx.path();                      (2)

    Map<String, String> pathMap = ctx.pathMap();  (3)

    String name = ctx.path("name").value();       (4)

    ...
  });
}
1 Access to the raw path string:
  • /a+b/a+b

  • /a b/a%20b (not decoded)

  • /%2F%2B/%2F%2B (not decoded)

2 Path as Value object:
  • /a+b{name=a+b}

  • /a b{name=a b} (decoded)

  • /%2F%2B{name=/+} (decoded)

3 Path as Map<String, String> object:
  • /a+b{name=a+b}

  • /a b{name=a b} (decoded)

  • /%2F%2B{name=/+} (decoded)

4 Path variable name as String:
  • /a+ba+b

  • /a ba b (decoded)

  • /%2F%2B/+ (decoded)

4.1.4. Query

Query String is part of the URI that start after the ? character.

Java
Kotlin
{
  get("/search", ctx -> {
    String queryString = ctx.queryString();                    (1)

    QueryString query = ctx.query();                           (2)

    Map<String, List<String>> queryMap = ctx.queryMultimap();  (3)

    String q = ctx.query("q").value();                         (4)

    SearchQuery searchQuery = ctx.query(SearchQuery.class);    (5)

    ...
  });
}

class SearchQuery {

   public final String q;

   public SearchQuery(String q) {
     this.q = q;
   }
}
1 Access to raw queryString:
  • /search"" (empty)

  • /search?q=a+b?q=a+b

  • /search?q=a b?q=a%20b (not decoded)

2 Query String as QueryString object:
  • /search{} (empty)

  • /search?q=a+b{q=a+b}

  • /search?q=a b{q=a b} (decoded)

3 Query string as multi-value map
  • /search{} (empty)

  • /search?q=a+b{q=[a+b]}

  • /search?q=a b{q=[a b]} (decoded)

4 Access to decoded variable q:
  • /searchBad Request (400). Missing value: "q"

  • /search?q=a+ba+b

  • /search?q=a ba b (decoded)

5 Query string as SearchQuery
  • /searchBad Request (400). Missing value: "q"

  • /search?q=a+bSearchQuery(q="a+b")

  • /search?q=a bSearchQuery(q="a b") (decoded)

4.1.5. Formdata

Formdata is expected to be in HTTP body, or for as part of the URI for GET requests.

Data is expected to be encoded as application/x-www-form-urlencoded.

Java
Kotlin
{
  post("/user", ctx -> {
    Formdata form = ctx.form();                             (1)

    Map<String, List<String>> formMap = ctx.formMultimap(); (2)

    String userId = ctx.form("id").value();                 (3)

    String pass = ctx.form("pass").value();                 (4)

    User user = ctx.form(User.class);                       (5)

    ...
  });
}

class User {

   public final String id;

   public final String pass;

   public User(String id, String pass) {
     this.id = id;
     this.pass = pass;
   }
}
curl -d "id=root&pass=pwd" -X POST http://localhost:8080/user
1 Form as Formdata{id=root, pass=pwd}
2 Form as multi-value map{id=root, pass=[pwd]}
3 Form variable idroot
4 Form variable passpwd
5 Form as User object ⇒ User(id=root, pass=pwd)

4.1.6. Multipart

Form-data must be present in the HTTP body and encoded as multipart/form-data:

Java
Kotlin
{
  post("/user", ctx -> {
    Multipart multipart = ctx.multipart();                            (1)

    Map<String, List<String> multipartMap = ctx.multipartMultimap();  (2)

    String userId = ctx.multipart("id").value();                      (3)

    String pass = ctx.multipart("pass").value();                      (4)

    FileUpload pic = ctx.file("pic");                                 (5)

    User user = ctx.multipart(User.class);                            (6)

    ...
  });
}

class User {

   public final String id;

   public final String pass;

   public final FileUpload pic;

   public User(String id, String pass, FileUpload pic) {
     this.id = id;
     this.pass = pass;
     this.pic = pic;
   }
}
curl -F id=root -F pass=root -F pic=@/path/to/local/file/profile.png http://localhost:8080/user
1 Form as Multipart{id=root, pass=pwd, pic=profile.png}
2 Form as multi-value map{id=root, pass=[pwd]}
3 Form variable idroot
4 Form variable passpwd
5 FileUpload variable pic
6 Form as User object ⇒ User(id=root, pass=pwd, pic=profile.png)
File Upload

File upload are available ONLY for multipart requests.

Java
Kotlin
  FileUpload pic = ctx.file("pic");         (1)

  List<FileUpload> pic = ctx.files("pic");  (2)

  List<FileUpload> files = ctx.files();     (3)
1 Single file upload named pic
2 Multiple file uploads named pic
3 All file uploads

4.1.7. Session

Session parameters are available via session() or sessionOrNull() methods. HTTP Session is covered in his own chapter, but here is a quick look:

Java
Kotlin
  Session session = ctx.session();                      (1)

  String attribute = ctx.session("attribute").value();  (2)
1 Find an existing Session or create one
2 Get a session attribute

4.1.8. Flash

Flash parameters are designed to transport success/error messages between requests. It is similar to a Session but the lifecycle is shorter: data is kept for only one request.

Java
Kotlin
  get("/", ctx -> {
    return ctx.flash("success").value("Welcome!"); (3)
  });

  post("/save", ctx -> {
    ctx.flash().put("success", "Item created");    (1)
    return ctx.sendRedirect("/");                  (2)
  });
1 Set a flash attribute: success
2 Redirect to home page
3 Display an existing flash attribute success or shows Welcome!

Flash attributes are implemented using an HTTP Cookie. To customize the cookie (its name defaults to jooby.flash) use the setFlashCookie(Cookie) method:

Java
Kotlin
{
  setFlashCookie(new Cookie("myflash").setHttpOnly(true));

  // or if you're fine with the default name
  getFlashCookie().setHttpOnly(true);
}

4.1.9. Parameter Lookup

You can search for parameters in multiple sources with an explicitly defined priority using the lookup() or lookup(String,ParamSource…​) method:

Java
Kotlin
get("/{foo}", ctx -> {
  String foo = ctx.lookup("foo", ParamSource.QUERY, ParamSource.PATH).value();
  return "foo is: " + foo;
});

get("/{foo}", ctx -> {
  String foo = ctx.lookup().inQuery().inPath().get("foo").value();
  return "foo is: " + foo;
});

In case of a request like /bar?foo=baz, foo is: baz will be returned since the query parameter takes precedence over the path parameter.

4.1.10. Client Certificates

If mutual TLS is enabled, you can access the client’s certificates from the context. The first certificate in the list is the peer certificate, followed by the ca certificates in the chain (the order is preserved).

Java
Kotlin
get("/{foo}", ctx -> {
  List<Certificate> certificates = ctx.getClientCertificates(); (1)
  Certificate peerCertificate = certificates.get(0);            (2)
});
1 Get all of the certificates presented by the client during the SSL handshake.
2 Get only the peer certificate.

4.2. Value API

The Value is an unified and type-safe API across all parameter types:

  • Header

  • Path

  • Query

  • Formdata/Multipart

For learning purpose we are going to show all the Value features using query parameters, but keep in mind these features apply to all the parameter types.

4.2.1. Single value

Single value is available via value() or [type]Value() functions:

Java
Kotlin
{
  get("/", ctx -> {
    String name = ctx.query("name").value();                          (1)

    float score = ctx.query("score").floatValue();                    (2)

    boolean enabled = ctx.query("enabled").booleanValue();            (3)

    BigDecimal decimal = ctx.query("decimal").value(BigDecimal::new); (4)
    ...
  });
}

The value() family methods always retrieve a value. If there is no value, a BadRequest(400) response is generated. So single value parameters are required:

1 Access to query parameter q and convert to String:
  • /?name=foofoo

  • /Bad Request(400): Missing value: "q"

2 Access to query parameter score and convert to float:
  • /?score=11.0

  • /?score=stringBad Request(400) (Type mismatch: cannot convert to number)

  • /Bad Request(400) (Required parameter score is not present)

3 Access to query parameter enabled and convert to boolean:
  • /?enabled=truetrue

  • /?enabled=stringBad Request(400) (Type mismatch: cannot convert to boolean)

  • /Bad Request(400): Missing value: "enabled"

4 Access to query parameter decimal and convert to BigDecimal:
  • /?decimal=2.32.3

  • /?decimal=stringBad Request(400) (Type mismatch: cannot convert to BigDecimal)

  • /Bad Request(400): Missing value: "decimal"

4.2.2. Default and Optional value

Default and optional value are available in two different ways:

  • Providing a default value

  • Requesting an java.util.Optional object

Java
Kotlin
{
  get("/search", ctx -> {
    String q = ctx.query("q").value("*:*");             (1)
    return q;
  });

  get("/search", ctx -> {
    Optional<String> q = ctx.query("q").toOptional();   (2)
    return q;
  });
}
1 Access to query variable q and convert to String with a default value of :.
  • /search?q=foofoo

  • /search:

2 Access to query variable q and convert to Optional<String>:
  • /search?q=fooOptional[foo]

  • /searchOptional.empty

4.2.3. Multiple values

Multiple values are available via functions:

  • toList(): Returns a java.util.List of values

  • toSet(): Returns a java.util.Set of values

Java
Kotlin
{
  get("/", ctx -> {
    List<String> q = ctx.query("q").toList();                            (1)

    List<Integer> n = ctx.query("n").toList(Integer.class);              (2)

    List<BigDecimal> decimals = ctx.query("d").toList(BigDecimal::new);  (3)

    ...
  });
}
1 Multi-value query parameter q as List<String>:
  • /[] (empty list)

  • /?q=foo[foo]

  • /?q=foo&q=bar[foo, bar]

2 Multi-value query parameter as List<Integer>
  • /[] (empty list)

  • /?n=1[1]

  • /?n=1&n=2[1, 2]

3 Multi-value query parameter as List<BigDecimal>
  • /[] (empty list)

  • /?d=1[1]

  • /?d=1&n=2[1, 2]

4.2.4. Structured data

The Value API provides a way to traverse and parse structured data:

/?user.name=root&user.pass=pass
Traversal
Java
Kotlin
{
  get("/", ctx -> {
    Value user = ctx.query("user");                  (1)
    String name  = user.get("name").value();         (2)
    String pass  = user.get("pass").value();         (3)
    String email = user.get("email").value("none");  (4)
    ...
  }}
}
1 Get the user node
2 Get the name value from user node
3 Get the pass value from user node
4 Get the mail value from user node. This is an optional value.

The get(String) takes a path and returns another value. The returning value may or may not exists.

Syntax

Structured data decoder supports dot and bracket notation:

Dot notation
?member.firstname=Pedro&member.lastname=Picapiedra
Bracket object notation
?member[firstname]=Pedro&member[lastname]=Picapiedra
Bracket array notation for tabular data
?members[0]firstname=Pedro&members[0]lastname=Picapiedra
POJO

Structured data decoder is able to reconstruct a POJO (Plain Old Java Object) from:

We are going to use a Group and Member objects to demonstrate how the decoder works:

Example
Java
Kotlin
class Member {
  public final String firstname;
  public final String lastName;

  public Member(String firstname, String lastname) {
    this.firstname = firstname;
    this.lastname = lastname;
  }
}

class Group {
  public final String id;
  public final List<Member> members;

  public Member(String id, List<Member> members) {
    this.id = id;
    this.members = members;
  }
}
Member parsing example:
/?firstname=Pedro&lastName=Picapiedra
Java
Kotlin
{
  get("/", ctx -> {
    Member member = ctx.query(Member.class);
    ...
  });
}
Member parsing example from base node:
/?member.firstname=Pedro&member.lastName=Picapiedra
Java
Kotlin
{
  get("/", ctx -> {
    Member member = ctx.query("member").to(Member.class);
    ...
  });
}

Tabular data uses the bracket array notation:

Member as tabular data:
/?[0]firstname=Pedro&[0]lastName=Picapiedra&[1]firstname=Pablo&[2]lastname=Marmol
Java
Kotlin
{
  get("/", ctx -> {
    List<Member> members = ctx.query().toList(Member.class);
    ...
  });
}
Group with members as tabular data:
/?id=flintstones&members[0]firstname=Pedro&members[0]lastName=Picapiedra
Java
Kotlin
{
  get("/", ctx -> {
    Group group = ctx.query(Group.class);
    ...
  });
}

The target POJO must follow one of these rules:

  • Has a zero argguments/default constructor, or

  • Has only one constructor

  • Has multiple constructors, but only one is annotated with Inject

The decoder matches HTTP parameters in the following order:

  • As constructor arguments

  • As setter method

HTTP parameter name which are not a valid Java identifier must be annotated with Named:

Java
Kotlin
class Member {
  public final String firstname;

  public final String lastname;

  public Member(@Named("first-name") String firstname, @Named("last-name") String lastname) {
    ....
  }
}

♡♡

4.3. Request Body

Raw request body is available via body() method:

Java
Kotlin
{
  post("/string", ctx -> {
    String body = ctx.body().value();        (1)
    ...
  });

  post("/bytes", ctx -> {
    byte[] body = ctx.body().bytes();        (2)
    ...
  });

  post("/stream", ctx -> {
    InputStream body = ctx.body().stream();  (3)
    ...
  });
}
1 HTTP Body as String
2 HTTP Body as byte array
3 HTTP Body as InputStream

This give us the raw body.

4.3.1. Message Decoder

Request body parsing is achieved using the MessageDecoder functional interface.

public interface MessageDecoder {

  <T> T decode(Context ctx, Type type) throws Exception;
}

MessageDecoder has a single decode method that takes two input arguments: (context, type) and returns a single result of the given type.

JSON example:
Java
Kotlin
{
  FavoriteJson lib = new FavoriteJson();           (1)

  decoder(MediaType.json, (ctx, type) -> {          (2)

    byte[] body = ctx.body().bytes();              (3)

    return lib.fromJson(body, type);               (4)
  });

  post("/", ctx -> {
    MyObject myObject = ctx.body(MyObject.class);  (5)
  });
}
1 Choose your favorite jackson library
2 Check if the Content-Type header matches application/json
3 Read the body as byte[]
4 Parse the body and use the requested type
5 Route handler now call the body(Type) function to trigger the decoder function

Jooby comes with a jackson decoder built on top of Jackson:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-jackson</artifactId>
  <version>3.5.0</version>
</dependency>

4.4. Response Body

Response body is generated from handler function:

Response body
Java
Kotlin
{
  get("/", ctx -> {
    ctx.setResponseCode(200);                  (1)

    ctx.setResponseType(MediaType.text);        (2)

    ctx.setResponseHeader("Date", new Date());  (3)

    return "Response";                          (4)
  });
}
1 Set status code to OK(200). This is the default status code
2 Set content-type to text/plain. This is the default content-type
3 Set the date header
4 Send a Response string to the client

4.4.1. Message Encoder

Response enconding is achieved using the MessageEncoder functional interface.

public interface MessageEncoder {

  byte[] encode(@NonNull Context ctx, @NonNull Object value) throws Exception;
}

MessageEncoder has a single encode method that accepts two input arguments: (context, result) and produces a result.

JSON example:
Java
Kotlin
{
  FavoriteJson lib = new FavoriteJson();           (1)

  encoder(MediaType.json, (ctx, result) -> {      (2)

    String json = lib.toJson(result);              (3)

    ctx.setDefaultResponseType(MediaType.json);    (4)

    return json;                                   (5)
  });

  get("/item", ctx -> {
    MyObject myObject = ...;
    return myObject;                               (6)
  });
}
1 Choose your favorite jackson library
2 Check if the Accept header matches application/json
3 Convert result to JSON
4 Set default Content-Type to application/json
5 Produces JSON response
6 Route handler returns a user defined type

Jooby comes with a jackson encoder built on top of Jackson:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-jackson</artifactId>
  <version>3.5.0</version>
</dependency>

5. MVC API

MVC API is an alternative way to define routes in Jooby. It generates source code to define and execute routes.

If you use Gradle 6.0 or a later version, you can leverage incremental annotation processing support, which means that Gradle only compiles classes that changed since the last compilation, and only runs annotation processing on those changed classes.

The annotation processor has two options allowing you to control incremental processing behavior:

build.gradle
Java
Kotlin
tasks.withType(JavaCompile) {
    options.compilerArgs += [
        '-parameters',
        '-Ajooby.incremental=true',
        '-Ajooby.services=true'
    ]
}

By setting jooby.incremental to false you can disable incremental processing entirely, which means the regardless what’s changed, the whole project is recompiled each time. Defaults to true.

The generated bytecode is responsible for registering routes, retrieving and invoking your controllers. Jooby loads these classes with Java’s service-provider loading facility by default. To make this work, a so-called provider configuration file needs to be created alongside with the generated classes. The content of this file is dependent on all MVC controllers, therefore the annotation processor must operate in aggregating mode, in which all generated classes are rewritten each time.

You may disable the generation of the provider configuration file by setting jooby.services to false (the default is true). This allows the annotation processor to run in isolating mode: if you change e.g. HelloController only, then only the class responsible for registering the routes for HelloController will be regenerated. This however will force Jooby to load the generated classes with reflection instead of the service-provider loading facility.

The package annotation contains all the annotations available for MVC routes.

MVC API:
Java
Kotlin
import io.jooby.annotation.*;

@Path("/mvc")                  (1)
public class Controller {

  @GET                         (2)
  public String sayHi() {
    return "Hello Mvc!";
  }
}

public class App extends Jooby {

  {
    mvc(new Controller_());   (3)
  }

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}
1 Set a path pattern. The @Path annotation is enable at class or method level
2 Add a HTTP method
3 Register/install the generated controller in the main application

5.1. Getting Started

To create a new MVC project open the jooby console and type:

jooby create myapp --mvc

The jooby console takes care of all configuration steps required by the annotation processing tool.

5.2. Registration

Mvc routes need to be registered (no classpath scanning). Registration is done from your application class:

Simple MVC route registration
Java
Kotlin
public class App extends Jooby {
  {
    mvc(new MyController_());
  }

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}

The mvc(MvcExtension) install the mvc route. The generated controller instantiate the controller or requires it when an jakarta.inject.Inject annotated controller is present.

5.3. Parameters

HTTP parameter provision is available via *Param annotations.

There is also a Param annotation which allows to retrieve parameters from multiple sources.

5.3.1. Header

Provisioning of headers is available via HeaderParam annotation:

Headers
Java
Kotlin
public class MyController {

  @GET
  public Object provisioning(@HeaderParam String token) {  (1)
    ...
  }
}
1 Access to HTTP header named token

Compared to JAX-RS the parameter name on @*Param annotation is completely optional, but required for non valid Java names:

Non valid Java name
Java
Kotlin
public class MyController {

  @GET
  public Object provisioning(@HeaderParam("Last-Modified-Since") long lastModifiedSince) {
    ...
  }
}

Provisioning of cookies is available via CookieParam annotation:

Cookies
Java
Kotlin
public class MyController {

  @GET
  public Object provisioning(@CookieParam String token) {  (1)
    ...
  }
}
1 Access to cookie named token

Compared to JAX-RS the parameter name on @*Param annotation is completely optional, but required for non valid Java names:

Non valid Java name
Java
Kotlin
public class MyController {

  @GET
  public Object provisioning(@CookieParam("token-id") String tokenId) {
    ...
  }
}

5.3.3. Path

For path parameters the PathParam annotation is required:

PathParam
Java
Kotlin
public class MyController {

  @Path("/{id}")
  public Object provisioning(@PathParam String id) {
    ...
  }
}

5.3.4. Query

For query parameters the QueryParam annotation is required:

QueryParam
Java
Kotlin
public class MyController {

  @Path("/")
  public Object provisioning(@QueryParam String q) {
    ...
  }
}

5.3.5. Formdata/Multipart

For formdata/multipart parameters the FormParam annotation is required:

QueryParam
Java
Kotlin
public class MyController {

  @Path("/")
  @POST
  public Object provisioning(@FormParam String username) {
    ...
  }
}

5.3.6. Body

Body parameter doesn’t require an annotation:

HTTP Body
Java
Kotlin
public class MyController {

  @Path("/")
  @POST
  public Object provisioning(MyObject body) {
    ...
  }
}

5.3.7. Bind

You can use the BindParam annotation which allow custom mapping from HTTP request.

Use the annotation
Java
Kotlin
public class Controller {

  @GET("/{foo}")
  public String bind(@BindParam MyBean bean) {
    return "with custom mapping: " + bean;
  }
}
Write the mapping function
Java
Kotlin
public record MyBean(String value) {

  public static MyBean of(Context ctx) {
    // build MyBean from HTTP request
  }
}

It works as:

  • The BindParam allow you to convert HTTP request to an Java Object in the way you wish

  • The annotation looks for public method/function that takes a Context as parameter and returns the same type required as parameter.

  • It looks in the parameter type or fallback into the controller class

Alternative you can specify the factory class:

   @BindParam(MyFactoryClass.class)

And/or function name:

   @BindParam(value = MyFactoryClass.class, fn = "fromContext")

5.3.8. Flash

Provisioning of flash attribute is available via FlashParam annotation:

Flash
Java
Kotlin
public class MyController {

  @GET
  public Object provisioning(@FlashParam String success) {  (1)
    ...
  }
}
1 Access to flash named success

5.3.9. Session

Provisioning of session attribute is available via SessionParam annotation:

Session Attribute
Java
Kotlin
public class MyController {

  @GET
  public Object provisioning(@SessionParam String userId) {  (1)
    ...
  }
}
1 Access to session attribute named userId

Provisioning of Session is available too:

Session Attribute
Java
Kotlin
public class MyController {

  @GET
  public Object provisioning(Session session) {  (1)
    ...
  }
}
1 If no session exists yet, new session will be created

To avoid this, just use java.util.Optional<Session> as type.

5.3.10. Context

Provisioning of context attributes is available via ContextParam annotation:

Context Attribute
Java
Kotlin
public class MyController {

  @GET
  public Object provisioning(@ContextParam String userId) {  (1)
    ...
  }
}
1 Access to context attribute named userId

Provisioning of all attributes is available too:

Context Attributes
Java
Kotlin
public class MyController {

  @GET
  public Object provisioning(@ContextParam Map<String, Object> attributes) {  (1)
    ...
  }
}
1 All context attributes must be set as arguments. They must be declared as Map<String, Object>

5.3.11. Multiple Sources

You can use the Param annotation to search for a parameter in multiple sources. The sources and their precedence can be specified as follows:

Multiple Sources
Java
Kotlin
public class FooController {

  @GET("/{foo}")
  public String multipleSources(@Param({ QUERY, PATH }) String foo) {
    return "foo is: " + foo;
  }
}

In case of a request like /bar?foo=baz, foo is: baz will be returned since the query parameter takes precedence over the path parameter.

5.4. Responses

5.4.1. Status Code

The default status code is Success(200), except for void methods with the @DELETE annotation which is set to No Content(204).

There are two options if you need a different status code:

5.4.2. NonBlocking

Method returning a CompletableFuture, Single, Maybe, Flowable, Mono or Flux is considered a non-blocking route.

Kotlin suspend functions are supported too:

Kotlin Coroutines
class SuspendMvc {
  @GET
  @Path("/delay")
  suspend fun delayed(ctx: Context): String {
    delay(100)
    return ctx.getRequestPath()
  }
}

fun main(args: Array<String>) {
  runApp(args) {
    use(SuspendMvc())
  }
}

A non-blocking route run on the event loop (by default) where blocking is NOT allowed. For more details please checkout the non-blocking responses section.

5.5. Execution model

The MVC routes follows the execution model described in Execution Model. To run application logic in the EVENT_LOOP:

EventLoop MVC route
Java
Kotlin
public class App extends Jooby {
  {
    mvc(new MyController());
  }

  public static void main(String[] args) {
    runApp(args, EVENT_LOOP, App::new);  (1)
  }
}
1 Start the application in the EVENT_LOOP execution mode

Similarly, if you need to run all mvc routes in the WORKER execution mode:

Worker mode MVC route
Java
Kotlin
public class App extends Jooby {
  {
    dispatch(() -> {
      mvc(new MyBlockingController());  (1)
    });
  }

  public static void main(String[] args) {
    runApp(args, EVENT_LOOP, App::new);
  }
}
1 Wrap the controller using the dispatch operator

One drawback with this approach is that the entire controller is now going to be executed in the worker or custom executor. For more fine grain control use the Dispatch annotation:

Dispatch annotation
Java
Kotlin
public class MyController {
  @GET("/nonblocking")
  public String nonblocking() {  (1)
    return "I'm nonblocking";
  }

  @GET("/blocking")
  @Dispatch
  public String blocking() {     (2)
    return "I'm blocking";
  }
}
1 MVC route run in EVENT_LOOP mode. Blocking is NOT allowed it.
2 MVC route run in WORKER mode. Blocking is allowed it.

The Dispatch annotation supports custom executor using an executor name.

Dispatch to custom executor
Java
Kotlin
public class MyController {
  @GET("/blocking")
  @Dispatch("single")         (1)
  public String blocking() {
    return "I'm blocking";
  }
}
1 Dispatch to an executor named it single

Executor must be registered using via services or executor utility method:

Custom executor registration
Java
Kotlin
{
  executor("single", Executors.newSingleThreadExecutor());

  mvc(new MyController());
}

The executor must be registered before the MVC route/controller.

5.6. JAX-RS Annotations

Alternative you can use JAX-RS annotations to define MVC routes.

Resource
Java
Kotlin
import javax.ws.rs.GET;
import javax.ws.rs.Path;

@Path("/jaxrs")
public class Resource {

  @GET
  public String getIt() {
    return "Got it!";
  }
}

Annotations work exactly like the Jooby MVC annotations, but keep in mind we don’t implement the JAX-RS specification and there is no immediate plan to do it.

The main reason to support JAX-RS annotations is to let you plug-in third-party tools that rely on them (mostly annotations processors).

5.7. Annotation Processor Options

Option Value Default Value Description

jooby.debug

boolean

true

Run processor in debug mode

jooby.incremental

boolean

true

Hints maven/gradle to do incremental compilation. Useful for development.

jooby.services

boolean

true

Generates META-INF/services metadata

jooby.skipAttributeAnnotations

array

[]

Skip annotation during byte code generation (i.e. don’t generate them as route attributes)

jooby.handler

string

[]

Add custom handler mapping.

jooby.mvcMethod

boolean

false

Set the Route.mvcMethod when true.

jooby.routerPrefix

string

Prefix for generated class

jooby.routerSuffix

string

_

Suffix for generated class

5.7.1. Setting options

Maven
Gradle
  <plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
      <annotationProcessorPaths>
        <!-- if using lombok, it must be placed before the jooby-apt -->
        <!-- if using avaje-inject, it must be placed after lombok, but before the jooby-apt -->
        <path>
          <groupId>io.jooby</groupId>
          <artifactId>jooby-apt</artifactId>
          <version>${jooby.version}</version>
        </path>
      </annotationProcessorPaths>
      <compilerArgs>
        <compilerArg>
          -Ajooby.debug=false
        </compilerArg>
        <compilerArg>
          -Ajooby.incremental=true
        </compilerArg>
        <compilerArg>
          -Ajooby.services=true
        </compilerArg>
        <compilerArg>
          -Ajooby.skipAttributeAnnotations=FooAnnotation,BarAnnotation
        </compilerArg>
        <compilerArg>
          -Ajooby.handler=myhandler
        </compilerArg>
      </compilerArgs>
    </configuration>
  </plugin>

Please note that the order of annotation processors is important. For example, if you’re using lombok and avaje-inject, the correct order should be: lombokavaje-injectjooby-apt

6. Static Files

Static files are available via assets(String) route. The assets route supports classpath and file-system resources.

Classpath resources:
Java
Kotlin
{
  assets("/static/*"); (1)
}
1 Map all the incoming request starting with /static/ to the root of classpath
  • GET /static/index.html/index.html

  • GET /static/js/file.js/js/file.js

  • GET /static/css/styles.css/css/styles.css

File system resources:
Java
Kotlin
{
  assets("/static/*", Paths.get("www")); (1)
}
1 Map all the incoming request starting with /static/ to a file system directory www
  • GET /static/index.htmlwww/index.html

  • GET /static/js/file.jswww/js/file.js

  • GET /static/css/styles.csswww/css/styles.css

Individual file mapping is supported too:

Classpath:
File system
{
  assets("/myfile.js", "/static/myfile.js");
}

6.1. Static Site

The assets route works for static sites too. Just need to use a special path mapping:

Classpath resources:
Java
Kotlin
{
  Path docs = Paths.get("docs"); (1)
  assets("/docs/?*", docs);      (2)
}
1 Serve from docs directory
2 Use the /?* mapping

The key difference is the /?* mapping. This mapping add support for base root mapping:

  • GET /docs/docs/index.html

  • GET /docs/index.html/docs/index.html

  • GET /docs/about.html/docs/about.html

  • GET /docs/note/docs/note/index.html

6.2. SPAs

The assets route works for single page applications (SPAs) too. Just need to use a special path mapping plus a fallback asset:

Classpath resources:
Java
Kotlin
{
  AssetSource docs = AssetSource.create(Paths.get("docs")); (1)
  assets("/docs/?*", new AssetHandler("index.html", docs)); (2)
}
1 Serve from docs directory
2 Use the /?* mapping and uses index.html as fallback asset

SPAs mode never generates a NOT FOUND (404) response, unresolved assets fallback to index.html

6.3. Options

The AssetHandler automatically handles E-Tag and Last-Modified headers. You can control these headers programmatically:

Asset handler options:
Java
Kotlin
{
  assets("/static/*", Paths.get("www"))
    .setLastModified(false)
    .setEtag(false);
}

The maxAge option set a Cache-Control header:

Cache control:
Java
Kotlin
{
  assets("/static/*", Paths.get("www"))
    .setMaxAge(Duration.ofDays(365))
}

There is also a setNoCache() method that explicitly forbids web browsers to cache assets.

You can use different cache configurations for different assets based on asset name if you specify a function via cacheControl(Function):

Per-asset cache control:
Java
Kotlin
{
  assets("/static/*", Paths.get("www"))
      .cacheControl(path -> {
        if (path.endsWith("dont-cache-me.html")) {
          return CacheControl.noCache(); // disable caching
        } else if (path.equals("foo.js")) {
          return CacheControl.defaults()
              .setETag(false)
              .setMaxAge(Duration.ofDays(365));
        } else {
          return CacheControl.defaults(); // AssetHandler defaults
        }
      });
}

The asset handler generates a 404 response code when requested path is not found. You can change this by throwing an exception or generating any other content you want:

Custom not found:
Java
Kotlin
{
  assets("/static/*", Paths.get("www"))
      .notFound(ctx -> {
        throw new MyAssetException();
      });

  error(MyAssetException.class, (ctx, cause, code) -> {
    // render MyAssetException as you want
  });
}

7. Templates

Templates are available via ModelAndView and requires a TemplateEngine implementation.

Java
Kotlin
{
  install(new MyTemplateEngineModule());           (1)

  get("/", ctx -> {
    MyModel model = ...;                           (2)
    return new ModelAndView("index.html", model);  (3)
  });
}
1 Install a template engine
2 Build the view model
3 Returns a ModelAndView instance

ModelAndView allows you to specify the desired locale used for template rendering:

Java
Kotlin
{
  install(new MyTemplateEngineModule());

  get("/", ctx -> {
    MyModel model = ...;
    return new ModelAndView("index.html", model)
        .setLocale(Locale.GERMAN);                (1)
  });
}
1 Set the preferred locale

If no locale is specified explicitly, a locale matched by the Accept-Language header of the current request is used.

Not all template engines support setting and using a specific locale. If you use such a template engine, the above configuration does not have any effect.

7.1. Template Engine

Template engine does the view rendering/encoding. Template engine extends a MessageEncoder by accepting a ModelAndView instance and produces a String result.

The extensions() method list the number of file extension that a template engine supports. Default file extension is: .html.

The file extension is used to locate the template engine, when a file extension isn’t supported an IllegalArgumentException is thrown.

The file extension allow us to use/mix multiple template engines too:

Java
Kotlin
{
  install(new HandlebarsModule());                 (1)
  install(new FreemarkerModule());                 (2)

  get("/first", ctx -> {
    return new ModelAndView("index.hbs", model);  (3)
  });

  get("/second", ctx -> {
    return new ModelAndView("index.ftl", model);  (4)
  });
}
1 Install Handlebars
2 Install Freemarker
3 Render using Handlebars, .hbs extension
4 Render using Freemarker, .ftl extension

Checkout all the available template engines provided by Jooby.

7.2. View Model

Since Jooby 3.1.x the model can be anything object you like, previous version requires to be always map. There are two implementations of ModelAndView:

  • ModelAndView(String view, Object model)

  • MapModelAndView(String view, Map<String, Object> model)

8. Session

Session is accessible via

Sessions have a lot of uses cases but the most commons are: authentication, storing information about current user, etc.

A session attribute must be a String or a primitive. The session doesn’t allow storing of arbitrary objects. It’s intended as a simple mechanism to store basic data (not an object graph).

Jooby provides the following :

  • In-Memory sessions - which you should combine with an a sticky sessions proxy if you plan to run multiple instances.

  • Cookie sessions signed with a secret key

  • JSON Web Token sessions

8.1. In-Memory Session

Default session store uses memory to save session data. This store:

  • Uses a cookie/header to read/save the session ID

  • Store session data in-memory

In-Memory Session
Java
Kotlin
{
  get("/", ctx -> {
    Session session = ctx.session();   (1)

    session.put("foo", "bar");         (2)

    return session.get("foo").value(); (3)
  });
}
1 Find an existing session or create a new session
2 Set a session attribute
3 Get a session attribute

Session token/ID is retrieved it from request cookie. Default session cookie is jooby.sid. To customize cookie details:

In-Memory Session with Custom Cookie
Java
Kotlin
{
  setSessionStore(SessionStore.memory(new Cookie("SESSION")));    (1)

  get("/", ctx -> {
    Session session = ctx.session();

    session.put("foo", "bar");

    return session.get("foo").value();
  });
}
1 Set an in-memory session store with a custom cookie named: SESSION

Alternative you can use a request header to retrieve a session token/ID:

In-Memory Session with HTTP Header
Java
Kotlin
{
  setSessionStore(SessionStore.memory(SessionToken.header("TOKEN")));    (1)

  get("/", ctx -> {
    Session session = ctx.session();

    session.put("foo", "bar");

    return session.get("foo").value();
  });
}
1 Session Token/ID comes from HTTP header TOKEN

You can mix cookie and header tokens:

Java
Kotlin
{
  setSessionStore(SessionStore.memory(SessionToken.comibe(SessionToken.cookie("SESSION"), SessionToken.header("TOKEN"))));    (1)

  get("/", ctx -> {
    Session session = ctx.session();

    session.put("foo", "bar");

    return session.get("foo").value();
  });
}
1 Session Token/ID comes from HTTP Cookie SESSION or HTTP header TOKEN (in that order)

8.2. Signed Session

This is a stateless session store that expects to find session token on each request. The server doesn’t keep any state.

  • Session data is retrieve/save from/into HTTP Cookie or Header

  • Session data is (un)signed with HmacSHA256. Key must be 256 bits long (32 bytes)

Data sign/unsign is done using sign(String,String) and unsign(String,String).

Usage
Java
Kotlin
{
  String secret = "super secret key";              (1)

  setSessionStore(SessionStore.signed(secret));    (2)

  get("/", ctx -> {
    Session session = ctx.session();

    session.put("foo", "bar");

    return session.get("foo").value();
  });
}
1 A secret key is required to signed the data
2 Creates a cookie session store using the secret

Like with memory session store you can use HTTP headers:

Signed with headers
Java
Kotlin
{
  String secret = "super secret key";                                            (1)

  setSessionStore(SessionStore.signed(secret, SessionToken.header("TOKEN")));    (2)

  get("/", ctx -> {
    Session session = ctx.session();

    session.put("foo", "bar");

    return session.get("foo").value();
  });
}

8.3. Stores

In addition to built-in memory stores we do provide:

  • Caffeine: In-memory session store using Caffeine cache.

  • JWT: JSON Web Token session store.

  • Redis: Redis session store.

9. Web Sockets

Adding a WebSocket:

WebSocket
Java
Kotlin
{
  ws("/ws", (ctx, configurer) -> {             (1)
    configurer.onConnect(ws -> {
      ws.send("Connected");                    (2)
    });

    configurer.onMessage((ws, message) -> {
      ws.send("Got " + message.value());       (3)
    });

    configurer.onClose((ws, statusCode) -> {
                                               (4)
    });

    configurer.onError((ws, cause) -> {
                                               // 5
    });
  });
}
1 Add a WebSocket handler. Useful to initialize resources
2 On WebSocket connect/open send a message back to client. Useful to initialize resources
3 On new message send back to client
4 WebSocket is about to close, you must free/release any acquire resources
5 WebSocket found a exception. Useful to log the error and provide an alternative response is the WebSocket is still open

You are free to access to HTTP context from WebSocket configurer or callback, but it is disallowed to modify the HTTP context or produces a response from it:

Context
Java
Kotlin
{
  ws("/ws/{key}", (ctx, configurer) -> {
    String key = ctx.path("key").value();           (1)
    String foo = ctx.session().get("foo").value();  (2)
    ...
  });
}
1 Access to path variable: key
2 Access to session variable: foo

9.1. Structured data

Structure data is supported using the Value API and the render() method:

JSON example
Java
Kotlin
import io.jooby.jackson.JacksonModule;

{
  install(new JackonModule());                         (1)

  ws("/ws", (ctx, configurer) -> {
    configurer.onMessage((ws, message) -> {
      MyObject myobject = message.to(MyObject.class);  (2)
      ws.render(myobject);                             (3)
    })
  });
}
1 Install Jackson module (required for JSON decoding/encoding)
2 Parse/decode message to MyObject
3 Encode myobject as JSON and send to client

Alternative you explicit tells with decoder/encoder to use consumes/produces attributes:

Context
Java
Kotlin
import io.jooby.jackson.JacksonModule;

{
  install(new JackonModule());                         (1)

  ws("/ws", (ctx, configurer) -> {
    configurer.onMessage((ws, message) -> {
      MyObject myobject = message.to(MyObject.class);  (2)
      ws.render(myobject);                             (3)
    })
  })
    .consumes(MediaType.json)
    .produces(MediaType.json);
}

Structure messages depends/requires a MessageDecoder and MessageEncoder. In this example both are provided by the JacksonModule.

9.2. Options

9.2.1. Connection Timeouts

Jooby timeouts idle connections that have no activity after 5 minutes. You can control this behaviour by setting the websocket.idleTimeout property:

application.conf
websocket.idleTimeout = 1h

9.2.2. Max size

Max size is set to 128K you can override it like:

application.conf
websocket.maxSize = 128K

10. Server-Sent Events

Server-sent Events (SSE) is a mechanism that allows the server to push data to the client once the client-server connection is established. After the connection has been established by the client, the server can send to the client whenever a new chunk of data is available. In contrast with websockets, SSE can only be used to send from the server to the client and not the other way round.

Server-Sent Events
Java
Kotlin
{
  sse("/sse", sse -> {            (1)
    sse.send("Welcome");          (2)
  });
}
1 Connection established
2 Send a message

10.1. Message Options

Additional message options are available via ServerSentMessage:

Server-Sent Events
Java
Kotlin
{
  sse("/sse", sse -> {
    sse.send(
        new ServerSentMessage("...")
            .setEvent("myevent")
            .setId(myId)
            .setRetry(1000)
    );
  });
}

Options are documented at Event stream format.

10.2. Connection Lost

The sse.onClose(Runnable) callback allows you to clean and release resources on connection close. A connection is closed when you call the sse.close() method or when the remote client closes the connection.

Connection Lost
Java
Kotlin
{
  sse("/sse", sse -> {
    sse.onClose(() -> {
      // clean up
    });
  });
}

10.3. Keep Alive

The keep alive time feature can be used to prevent connections from timing out:

Keep Alive
Java
Kotlin
{
  sse("/sse", sse -> {
    sse.keepAlive(15, TimeUnit.SECONDS)
  });
}

The previous example will send a ':' message (empty comment) every 15 seconds to keep the connection alive. If the client drops the connection, then the sse.onClose(Runnable) event will be fired.

This feature is useful when you want to detect close events without waiting for the next time you send an event. If on the other hand your application already generates events every 15 seconds, the use of keep alive is unnecessary.

11. Execution Model

Jooby is a flexible performant microframework providing both blocking and non-blocking APIs for building web applications in Java and Kotlin.

In this chapter we are going to learn about Jooby execution model, more specifically:

  • Execute code on the event loop

  • Safely execution of blocking code

  • Working with non-blocking types, like: CompletableFuture, Reactive Streams, Kotlin Coroutines, etc.

11.1. Mode

11.1.1. Event Loop

The EVENT_LOOP mode allows us to run a route handler from the event loop (a.k.a as non-blocking mode).

Java
Kotlin
import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;

public class App extends Jooby {

  {
    get("/", ctx -> "I'm non-blocking!" );
  }

  public static void main(String[] args) {
    runApp(args, EVENT_LOOP, App::new);
  }
}

The EVENT_LOOP mode is the more advanced execution mode and requires you carefully design and implement your application due to that BLOCKING IS NOT ALLOWED

What if you need to block?

The dispatch(Runnable) operator moves execution to a worker executor which allows to do blocking calls:

Java
Kotlin
import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;

public class App extends Jooby {

  {
    get("/", ctx -> {
      return "I'm non-blocking!";
    });

    dispatch(() -> {
      // All the routes defined here are allowed to block:

      get("/db-list", ctx -> {
        /** Safe to block! */
        Object result = ...; // Remote service, db call, etc..
        return result;
      });

    });
  }

  public static void main(String[] args) {
    runApp(args, EVENT_LOOP, App::new);
  }
}

By default, the dispatch(Runnable) operator moves execution to the server worker executor (executor provided by web server).

You can provide your own worker executor at application level or at dispatch level:

Java
Kotlin
import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;

public class App extends Jooby {

  {
    // Application level executor
    worker(Executors.newCachedThreadPool());

    // Dispatch to application level executor which is cached thread pool
    dispatch(() -> {
      ...
    });

    // Dispatch to a explicit executor
    Executor cpuIntensive = Executors.newSingleThreadExecutor();
    dispatch(cpuIntesive, () -> {
      ...
    });
  }

  public static void main(String[] args) {
    runApp(args, EVENT_LOOP, App:new);
  }
}

11.1.2. Worker

The WORKER mode allows us to do blocking calls from a route handler (a.k.a blocking mode). You just write code without worrying about blocking calls.

Java
Kotlin
import static io.jooby.ExecutionMode.WORKER;
import static io.jooby.Jooby.runApp;

public class App extends Jooby {

  {
    get("/", ctx -> {
      /** Safe to block! */
      Object result = // Remote service, db call, etc..
      return result;
    });
  }

  public static void main(String[] args) {
    runApp(args, WORKER, App::new);
  }
}

Like with EVENT_LOOP mode, you can provide your own worker executor:

Java
Kotlin
import static io.jooby.ExecutionMode.WORKER;
import static io.jooby.Jooby.runApp;

public class App extends Jooby {

  {
    worker(Executors.newCachedThreadPool());

    get("/", ctx -> {
      /** Safe to block from cached thread pool! */
      Object result = // Remote service, db call, etc..
      return result;
    });
  }

  public static void main(String[] args) {
    runApp(args, WORKER, App::new);
  }
}

While running in WORKER mode, Jooby internally does the dispatch call to the worker executor. This is done per route, not globally.

11.1.3. Default

The DEFAULT execution mode is a mix between WORKER and EVENT_LOOP modes. This (as name implies) is the default execution mode in Jooby.

Jooby detects the route response type and determines which execution mode fits better.

If the response type is considered non-blocking, then it uses the event loop. Otherwise, it uses the worker executor.

A response type is considered non-blocking when route handler produces:

Java
Kotlin
import static io.jooby.Jooby.runApp;

public class App extends Jooby {

  {
    get("/non-blocking", ctx -> {
      return CompletableFuture
          .supplyAsync(() -> "I'm non-blocking!")  (1)
    });

    get("/blocking", ctx -> {
      return "I'm blocking";                       (2)
    });
  }

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}
1 CompletableFuture is a non-blocking type, run in event loop
2 String is a blocking type, run in worker executor

You are free to use non-blocking types in all the other execution mode too. Non-blocking response types are not specific to the default mode execution. All the default mode does with them is to dispatch or not to a worker executor.

11.2. Worker Executor

This section described some details about the default worker executor provided by web server. The worker executor is used when:

Each web server provides a default worker executor:

  • Netty: The Netty server implementation multiply the number of available processors (with a minimum of 2) by 8.

workerThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2) * 8

For example 8 cores gives us 64 worker threads.

  • Undertow: The Undertow server implementation multiply the number of available processors by 8.

workerThreads = Runtime.getRuntime().availableProcessors() * 8

For 8 cores gives us 64 worker threads.

  • Jetty: The Jetty server implementation uses the default configuration with 200 worker threads.

These are sensible defaults suggested by the server implementation. If you need to increase/decrease worker threads:

Java
Kotlin
{
  configureServer(server -> {
    server.workerThreads(Number);
  });
}

12. Responses

This chapter covers some special response types, like raw responses, streaming, file download, non-blocking, etc…​

12.1. Raw

Raw responses are NOT processed by a message encoder. These response types are considered raw:

  • String/CharSequence

  • byte[]

  • java.nio.ByteBuffer/io.netty.buffer.ByteBuf

  • java.io.File/java.io.InputStream/java.nio.file.Path/java.nio.channels.FileChannel

Generate a JSON String from handler
Java
Kotlin
{
  get("/json", ctx -> {
    ctx.setContentType(MediaType.json);
    return "{\"message\": \"Hello Raw Response\"}";
  });
}

No matter if there is a JSON encoder installed, a raw response is always send directly to client.

12.2. Streaming / Chunked

Streaming/chunked API is available via:

Only one of these methods must be call it per request. At the time you call one of these methods Jooby automatically adds the Transfer-Encoding: chunked header when Content-Length is missing.

All the three APIs have a close method. You must call it once you finish.

Writer example
Java
Kotlin
{
  get("/chunk", ctx -> {
    try(Writer writer = ctx.responseWriter()) { (1)
      writer.write("chunk1");                   (2)
      ...
      writer.write("chunkN");
    }

    return ctx;                                 (3)
  });
}
1 Get the Writer inside a try-with-resources statement, so close it automatically.
2 Write chunks
3 Return the Context

There is an overloaded version (for Java mainly) that let you skip the try-with-resources and automatically close the writer/stream:

Writer example
Java
Kotlin
{
  get("/chunk", ctx -> {
    return ctx.responseWriter(writer -> { (1)
      writer.write("chunk1");             (2)
      ...
      writer.write("chunkN");
    });
  });
}

12.3. File download

The FileDownload is used to generate file downloads, i.e. responses with Content-Disposition header. You can use the convenience subclasses AttachedFile or InlineFile to set the header value to attachment or inline respectively.

File download example
Java
Kotlin
{
  get("/download-file", ctx -> {
    Path source = Paths.get("logo.png");
    return new AttachedFile(source);               (1)
  });

  get("/view-stream", ctx -> {
    InputStream source = ...;
    return new InlineFile("myfile.txt", source); (2)
  });
}
1 Send a download from an InputStream
2 Send a download from a File

Another possibility is to use one of the static builder methods of FileDownload and specify the download type (attachment or inline) later.

File download with builder method
Java
Kotlin
FileDownload.Builder produceDownload(Context ctx) {
  return FileDownload.build(...);
}

{
  get("/view", ctx -> produceDownload(ctx).inline());

  get("/download", ctx -> produceDownload(ctx).attachment());
}

12.4. NonBlocking

Non-blocking responses are a new feature of Jooby 2.x.

From user point of view there is nothing special about them, you just write your route handler as usually do with blocking types.

Before we jump to each of the supported types, we need to learn what occurs in the pipeline when there is a non-blocking route handler.

In event loop
Java
Kotlin
{
  mode(EVENT_LOOP);                  (1)

  use(ReactiveSupport.concurrent()); (2)

  get("/non-blocking", ctx -> {

    return CompletableFuture         (3)
        .supplyAsync(() -> {
          ...                        (4)
        });
  })
}
1 App run in event loop
2 Indicates we want to go non-blocking and handle CompletableFuture responses.
3 Value is provided from event loop. No blocking code is permitted
4 Value is computed/produces from completable future context

Running your App3508 in worker mode works identically, except for we are able to do blocking calls:

In worker mode
Java
Kotlin
{
  mode(WORKER);                      (1)

  use(ReactiveSupport.concurrent()); (2)

  get("/blocking", ctx -> {

    return CompletableFuture         (3)
        .supplyAsync(() -> {
          ...                        (4)
        });
  })
}
1 App run in worker mode
2 Indicates we want to go non-blocking and handle CompletableFuture responses.
3 Value is provided from worker mode. Blocking code is permitted
4 Value is computed/produces from completable future context

Running your App3508 in default mode works identically to running in the event loop mode:

In default mode
Java
Kotlin
{
  mode(DEFAULT);                     (1)

  use(ReactiveSupport.concurrent()); (2)

  get("/non-blocking", ctx -> {

    return CompletableFuture         (3)
        .supplyAsync(() -> {
          ...                        (4)
        });
  })
}
1 App run in worker mode
2 Indicates we want to go non-blocking and handle CompletableFuture responses.
3 Value is provided from worker mode. Blocking code is permitted
4 Value is computed/produces from completable future context

The default mode mimics the event loop mode execution when route produces a non-blocking type.

12.4.1. CompletableFuture

CompletableFuture is considered a non-blocking type which is able to produces a single result:

Java
Kotlin
{
  use(ReactiveSupport.concurrent());

  get("/non-blocking", ctx -> {
    return CompletableFuture
        .supplyAsync(() -> "Completable Future!")
        .thenApply(it -> "Hello " + it);
  })
}

Completable future responses require explicit handler setup ONLY in script/lambda routes. For MVC routes you don’t need to setup any handler. It is done automatically based on route response type.

12.4.2. Mutiny

1) Add the SmallRye Mutiny dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-mutiny</artifactId>
  <version>3.5.0</version>
</dependency>

2) Write code:

Uni
Java
Kotlin
import io.jooby.mutiny;
import io.smallrye.mutiny.Uni;

{
  // Add response handler:
  use(Mutiny.mutiny());

  get("/non-blocking", ctx -> {
    return Uni.createFrom()
       .completionStage(supplyAsync(() -> "Uni"))
       .map(it -> "Hello " + it);
  })
}
Multi
Java
Kotlin
import io.jooby.mutiny;
import io.smallrye.mutiny.Multi;

{
  // Add response handler:
  use(Mutiny.mutiny());

  get("/non-blocking", ctx -> {
    return Multi.createFrom().range(1, 11)
        .map(it -> it + ", ");
  })
}

For Multi, Jooby builds a chunked response. That:

  1. Set the Transfer-Encoding: chunked header

  2. Each item means new chunk send it to client

Mutiny responses require explicit handler setup ONLY in script/lambda routes. For MVC routes you don’t need to setup any handler. It is done automatically based on route response type.

12.4.3. RxJava

1) Add the RxJava dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-rxjava3</artifactId>
  <version>3.5.0</version>
</dependency>

2) Write code:

Single
Java
Kotlin
import io.jooby.rxjava3.Reactivex;

{
  use(Reactivex.rx());

  get("/non-blocking", ctx -> {
    return Single
        .fromCallable(() -> "Single")
        .map(it -> "Hello " + it);
  })
}
Flowable
Java
Kotlin
import io.jooby.rxjava3.Reactivex;

{
  use(Reactivex.rx());

  get("/non-blocking", ctx -> {
    return Flowable.range(1, 10)
        .map(it -> it + ", ");
  })
}

For Flowable, Jooby builds a chunked response. That:

  1. Set the Transfer-Encoding: chunked header

  2. Each item means new chunk send it to client

Rx responses require explicit handler setup ONLY in script/lambda routes. For MVC routes you don’t need to setup any handler. It is done automatically based on route response type.

12.4.4. Reactor

1) Add the Reactor dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-reactor</artifactId>
  <version>3.5.0</version>
</dependency>

2) Write code:

Mono
Java
Kotlin
import io.jooby.Reactor;

{
  use(Reactor.reactor());

  get("/non-blocking", ctx -> {
    return Mono
        .fromCallable(() -> "Mono")
        .map(it -> "Hello " + it);
  })
}
Flux
Java
Kotlin
import io.jooby.Reactor;

{
  use(Reactor.reactor())

  get("/non-blocking", ctx -> {
    return Flux.range(1, 10)
        .map(it -> it + ", ");
  })
}

For Flux, Jooby builds a chunked response. That:

  1. Set the Transfer-Encoding: chunked header

  2. Each item means new chunk send it to client

Reactor responses require explicit handler setup ONLY in script/lambda routes. For MVC routes you don’t need to setup any handler. It is done automatically based on route response type.

12.4.5. Kotlin Coroutines

Coroutine handler:
{
  coroutine {
    get("/") {
      delay(100)           (1)
      "Hello Coroutines!"  (2)
    }
  }
}
1 Call a suspending function
2 Send response to client
Here is another example with an extension and suspending function:
{
  coroutine {
    get("/") {
      ctx.doSomething()         (1)
    }
  }
}

suspend fun Context.doSomething(): String {
  delay(100)                  (2)
  return "Hello Coroutines!"  (3)
}
1 Call extension suspending function
2 Call a suspending function or do a blocking call
3 Send response to client

A coroutine works like any of the other non-blocking types. You start Jooby using the event loop or default mode, Jooby creates a coroutine context to execute it.

Jooby uses the worker executor to creates a coroutine context. As described in worker executor section this is provided by the web server implementation unless you provided your own.

Coroutines with custom executor:
{
  worker(Executors.newCachedThreadPool())

  coroutine {
    get("/") {
      val n = 5 * 5        (1)
      delay(100)           (2)
      "Hello Coroutines!"  (3)
    }
  }
}
1 Statement run in the worker executor (cached thread pool)
2 Call a suspending function
3 Produces a response

Coroutines always run in the worker executor. There is an experimental API where coroutines run in the caller thread(event loop in this case) until a suspending function is found.

Jooby allows you to use this experimental API by setting the coroutineStart option:

UNDISPATCHED
{
  coroutine(CoroutineStart.UNDISPATCHED) {
    get("/") {
      val n = 5 * 5        (1)
      delay(100)           (2)
      "Hello Coroutines!"  (3)
    }
  }
}
1 Statement run in the event loop (caller thread)
2 Call a suspending function and dispatch to worker executor
3 Produces a response from worker executor

You can also extend the CoroutineContext in which the coroutine routes run:

launchContext
{
  coroutine {
    launchContext { MDCContext() } (1)

    get("/") {
      ...
    }
  }
}
1 The lambda is run before launching each coroutine, so it can customize the CoroutineContext for the request, e.g. store/restore MDC, transaction, or anything else that your handlers need.

♡ ♡!

12.5. Send methods

Jooby provides a family of send() methods that produces a response via side effects.

send text
Java
Kotlin
{
  get("/", ctx -> {
    return ctx.send("Hello World!");
  });
}

Beside we operate via side effects, the route still returns something. This is required because a route handler is a function which always produces a result.

All the send methods returns the current Context, this signal Jooby that we want to operate via side effects ignoring the output of the route handler.

Family of send methods include:

13. Error Handler

Jooby catches application exception using the ErrorHandler class. The DEFAULT error handler produces simple HTML page or JSON based on the value of the ACCEPT header and log the exception.

HTML:
Not Found
message: Page not found
status code: 404
JSON:
{
  "message": "Page not found",
  "status": 404,
  "reason": "Not Found"
}
Log:
GET /xx 404 Not Found
io.jooby.StatusCodeException: Not found
	at ...
	at ...
	at ...
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

The StatusCodeException works as generic exception that let you specify an status code.

throw new StatusCodeException(StatusCode.FORBIDDEN);

throw new StatusCodeException(StatusCode.NOT_FOUND);

...

These exception types have a default status code:

  • IllegalArgumentException: BAD_REQUEST(400) (or sub-classes of it)

  • NoSuchElementException: BAD_REQUEST(400) (or sub-classes of it)

  • FileNotFound: NOT_FOUND(404) (or sub-classes of it)

  • Exception: SERVER_ERROR(500) (or sub-classes of it)

To set a custom status code, an entry should be added it to the error code map:

{
  errorCode(MyException.class, StatusCode.XXX);
}

13.1. Custom Error Handler

You can provide your own error handler using the error(ErrorHandler) method:

Error Handler
Java
Kotlin
{
  error((ctx, cause, statusCode) -> {                                      (1)
    Router router = ctx.getRouter();
    router.getLog().error("found `{}` error", statusCode.value(), cause);  (2)
    ctx.setResponseCode(statusCode);
    ctx.send("found `" + statusCode.value() + "` error");                  (3)
  });
}
1 Add a global/catch-all exception handler
2 Log the error to logging system
3 Send an error response to the client

You can use the render(Object) object which looks for a registered MessageEncoder or TemplateEngine.

The next example produces a HTML or JSON response based on the value of the Accept header.

Error with content negotiation
Java
Kotlin
import static io.jooby.MediaType.json;
import static io.jooby.MediaType.html;

{
  install(new MyTemplateEngineModule());                      (1)

  install(new MyJsonModule());                                (2)

  error((ctx, cause, statusCode) -> {
    Router router = ctx.getRouter();
    router.getLog().error("found `{}` error", statusCode.value(), cause);

    if (ctx.accept(json)) {                                   (3)
      Map error = ...;
      ctx.render(error);                                      (4)
    } else {
      // fallback to html
      Map error = ...;
      ctx.render(new ModelAndView("error.template", error));  (5)
    }
  });
}
1 Install one of the available template engines
2 Install one of the available json modules
3 Test if the accept header matches the application/json content type
4 Render json if matches
5 Render html as fallback

13.2. Catch by Code

In addition to the generic/global error handler you can catch specific status code:

Status Code Error Handler
Java
Kotlin
import static io.jooby.StatusCode.NOT_FOUND;
{
  error(NOT_FOUND, (ctx, cause, statusCode) -> {
    ctx.send(statusCode);   (1)
  });
}
1 Send 404 response to the client

Here we kind of silence all the 404 response due we don’t log anything and send an empty response.

The send(StatusCode) method send an empty response to the client

13.3. Catch by Exception

In addition to the generic/global error handler you can catch specific exception type:

Exception Handler
Java
Kotlin
{
  error(MyException.class, (ctx, cause, statusCode) -> {
    // log and process MyException
  });
}

14. Problem Details

Most APIs have a way to report problems and errors, helping the user understand when something went wrong and what the issue is. The method used depends on the API’s style, technology, and design. Handling error reporting is an important part of the overall API design process.

You could create your own error-reporting system, but that takes time and effort, both for the designer and for users who need to learn the custom approach. Thankfully, there’s a standard called IETF RFC 7807 (later refined in RFC 9457) that can help.

By adopting RFC 7807, API designers don’t have to spend time creating a custom solution, and users benefit by recognizing a familiar format across different APIs. If it suits the API’s needs, using this standard benefits both designers and users alike.

Jooby provides built-in support for Problem Details.

14.1. Set up ProblemDetails

To enable the ProblemDetails, simply add the following line to your configuration:

application.conf
problem.details.enabled = true

This is the bare minimal configuration you need. It enables a global error handler that catches all exceptions, transforms them into Problem Details compliant format and renders the response based on the Accept header value. It also sets the appropriate content-type in response (e.g. application/problem+json, application/problem+xml)

All supported settings include:

application.conf
problem.details {
  enabled = true
  log4xxErrors = true                                (1)
  muteCodes = [401, 403]                             (2)
  muteTypes = ["com.example.MyMutedException"]       (3)
}
1 By default, only server errors (5xx) will be logged. You can optionally enable the logging of client errors (4xx). If DEBUG logging level is enabled, the log will contain a stacktrace as well.
2 You can optionally mute some status codes completely.
3 You can optionally mute some exceptions logging completely.

14.2. Creating problems

HttpProblem class represents the RFC 7807 model. It is the main entity you need to work with to produce the problem.

14.2.1. Static helpers

There are several handy static methods to produce a simple HttpProblem:

  • HttpProblem.valueOf(StatusCode status) - will pick the title by status code. Don’t overuse it, the problem should have meaningful title and detail when possible.

  • HttpProblem.valueOf(StatusCode status, String title) - with custom title

  • HttpProblem.valueOf(StatusCode status, String title, String detail) - with title and detail

HttpProblem extends RuntimeException so you can naturally throw it (as you do with exceptions):

Java
Kotlin
import io.jooby.problem.HttpProblem;

get("/users/{userId}", ctx -> {
  var userId = ctx.path("userId").value();
  User user = userRepository.findUser(userId);

  if (user == null) {
    throw HttpProblem.valueOf(StatusCode.NOT_FOUND,
      "User Not Found",
      "User with ID %s was not found in the system.".formatted(userId)
    );
  }
  ...
});

Resulting response:

{
    "timestamp": "2024-10-05T14:10:41.648933100Z",
    "type": "about:blank",
    "title": "User Not Found",
    "status": 404,
    "detail": "User with ID 123 was not found in the system.",
    "instance": null
}

14.2.2. Builder

Use builder to create a rich problem instance with all properties:

throw HttpProblem.builder()
  .type(URI.create("http://example.com/invalid-params"))
  .title("Invalid input parameters")
  .status(StatusCode.UNPROCESSABLE_ENTITY)
  .detail("'Name' may not be empty")
  .instance(URI.create("http://example.com/invalid-params/3325"))
  .build();

14.3. Adding extra parameters

RFC 7807 has a simple extension model: APIs are free to add any other properties to the problem details object, so all properties other than the five ones listed above are extensions.

However, variadic root level fields are usually not very convenient for (de)serialization (especially in statically typed languages). That’s why HttpProblem implementation grabs all extensions under a single root field parameters. You can add parameters using builder like this:

throw HttpProblem.builder()
  .title("Order not found")
  .status(StatusCode.NOT_FOUND)
  .detail("Order with ID $orderId could not be processed because it is missing or invalid.")
  .param("reason", "Order ID format incorrect or order does not exist.")
  .param("suggestion", "Please check the order ID and try again")
  .param("supportReference", "/support")
  .build();

Resulting response:

{
  "timestamp": "2024-10-06T07:34:06.643235500Z",
  "type": "about:blank",
  "title": "Order not found",
  "status": 404,
  "detail": "Order with ID $orderId could not be processed because it is missing or invalid.",
  "instance": null,
  "parameters": {
    "reason": "Order ID format incorrect or order does not exist.",
    "suggestion": "Please check the order ID and try again",
    "supportReference": "/support"
  }
}

14.4. Adding headers

Some HTTP codes (like 413 or 426) require additional response headers, or it may be required by third-party system/integration. HttpProblem support additional headers in response:

throw HttpProblem.builder()
  .title("Invalid input parameters")
  .status(StatusCode.UNPROCESSABLE_ENTITY)
  .header("my-string-header", "string")
  .header("my-int-header", 100)
  .build();

14.5. Respond with errors details

RFC 9457 finally described how errors should be delivered in HTTP APIs. It is basically another extension errors on a root level. Adding errors is straight-forward using error() or errors() for bulk addition in builder:

throw HttpProblem.builder()
  ...
  .error(new HttpProblem.Error("First name cannot be blank", "/firstName"))
  .error(new HttpProblem.Error("Last name is required", "/lastName"))
  .build();

In response:

{
  ...
  "errors": [
    {
      "detail": "First name cannot be blank",
      "pointer": "/firstName"
    },
    {
      "detail": "Last name is required",
      "pointer": "/lastName"
    }
  ]
}

If you need to enrich errors with more information feel free to extend HttpProblem.Error and make your custom errors model.

14.6. Custom Exception to HttpProblem

Apparently, you may already have many custom Exception classes in the codebase, and you want to make them Problem Details compliant without complete re-write. You can achieve this by implementing HttpProblemMappable interface. It allows you to control how exceptions should be transformed into HttpProblem if default behaviour doesn’t suite your needs:

import io.jooby.problem.HttpProblemMappable;

public class MyException implements HttpProblemMappable {

  public HttpProblem toHttpProblem() {
    return HttpProblem.builder()
      ...
      build();
  }

}

14.7. Custom Problems

Extending HttpProblem and utilizing builder functionality makes it really easy:

public class OutOfStockProblem extends HttpProblem {

  private static final URI TYPE = URI.create("https://example.org/out-of-stock");

  public OutOfStockProblem(final String product) {
    super(builder()
      .type(TYPE)
      .title("Out of Stock")
      .status(StatusCode.BAD_REQUEST)
      .detail(String.format("'%s' is no longer available", product))
      .param("suggestions", List.of("Coffee Grinder MX-17", "Coffee Grinder MX-25"))
    );
  }
}

14.8. Custom Exception Handlers

All the features described above should give you ability to rely solely on built-in global error handler. But, in case you still need custom exception handler for some reason, you still can do it:

{
    ...
    error(MyCustomException.class, (ctx, cause, code) -> {
      MyCustomException ex = (MyCustomException) cause;

      HttpProblem problem = ... ;                                      (1)

      ctx.getRouter().getErrorHandler().apply(ctx, problem, code);     (2)
    });
}
1 Transform exception to HttpProblem
2 Propagate the problem to ProblemDetailsHandler. It will handle the rest.

Do not attempt to render HttpProblem manually, it is strongly discouraged. HttpProblem is derived from the RuntimeException to enable ease of HttpProblem throwing. Thus, thrown HttpProblem will also contain a stacktrace, if you render HttpProblem as is - it will be rendered together with stacktrace. It is strongly advised not to expose the stacktrace to the client system. Propagate the problem to global error handler and let him take care of the rest.

15. Handlers

This section describes some built-in handler provided by Jooby.

15.1. AccessLogHandler

The AccessLogHandler logs incoming requests using the NCSA format (a.k.a common log format).

Usage
Java
Kotlin
import io.jooby.Jooby;
import io.jooby.AccessLogHandler;
...
{

  use(new AccessLogHandler()); (1)

  get("/", ctx -> {
    ...
  });
}
1 Install AccessLogHandler

Prints a message like:

127.0.0.1 - - [04/Oct/2016:17:51:42 +0000] "GET / HTTP/1.1" 200 2 3

Message is represented by:

  • Remote Address.

  • User ID (or dash when missing)

  • Date and time

  • HTTP method, requestPath and protocol

  • Response Status Code

  • Response Content-Length (or dash when missing)

  • Time took to process the request in milliseconds

Extra request or response headers can be appended at the end using the available methods:

If you run behind a reverse proxy that has been configured to send the X-Forwarded-* header, please consider to use trust proxy option.

15.2. CorsHandler

Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell a browser to let a web application running at one origin (domain) have permission to access selected resources from a server at a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, or port) than its own origin.

Jooby supports CORS out of the box. By default, CORS requests will be rejected. To enable processing of CORS requests, use the CorsHandler:

CorsExample
Java
Kotlin
import io.jooby.Jooby;
import io.jooby.CorsHandler;
...
{

  use(new CorsHandler()); (1)

  path("/api", () -> {
    // API methods
  });
}
1 Install CorsHandler with defaults options

Default options are:

  • origin: *

  • credentials: true

  • allowed methods: GET, POST

  • allowed headers: X-Requested-With, Content-Type, Accept and Origin

  • max age: 30m;

To customize default options use Cors:

Cors options
Java
Kotlin
import io.jooby.Jooby;
import io.jooby.CorsHandler;
...
{
  Cors cors = new Cors()
     .setMethods("GET", "POST", "PUT");      (1)

  use(new CorsHandler(cors));          (2)

  path("/api", () -> {
    // API methods
  });
}
1 Specify allowed methods
2 Pass cors options to cors handler

Optionally cors options can be specified in the application configuration file:

application.conf
cors {
  origin: "*"
  credentials: true
  methods: [GET, POST],
  headers: [Content-Type],
  maxAge: 30m
  exposedHeaders: [Custom-Header]
}
Loading options
Java
Kotlin
import io.jooby.Jooby;
import io.jooby.CorsHandler;
...
{
  Cors cors = Cors.from(getConfig());  (1)

  use(new CorsHandler(cors));

  path("/api", () -> {
    // API methods
  });
}
1 Load cors options from application configuration file

15.3. CsrfHandler

The Cross Site Request Forgery Handler helps to protect from (CSRF) attacks. Cross-site request forgeries are a type of malicious exploit whereby unauthorized commands are performed on behalf of an authenticated user.

Jooby automatically generates a CSRF "token" for each active user session managed by the application. This token is used to verify that the authenticated user is the one actually making the requests to the application.

Anytime you define an HTML form in your application, you should include a hidden CSRF token field in the form so that the CSRF protection middleware can validate the request

CSRF
<form method="POST" action="...">
    <input name="csrf" value="{{csrf}}" type="hidden" />
    ...
</form>

The csrf is a request attribute created by the CsrfHandler handler and rendered by a template engine. Here {{csrf}} we use Handlebars template engine (as example).

The CsrfHandler handler, will automatically verify that the token in the request input matches the token stored in the session.

The token defaults name is csrf and can be provided as:

  • header

  • cookie

  • form parameter

Configuration methods:

15.4. GracefulShutdown

The GracefulShutdown extension waits for existing requests to finish.

Example
Java
Kotlin
import io.jooby.Jooby;
import io.jooby.GracefulShutdown;
...
{

  install(new GracefulShutdown()); (1)

  // other routes go here
}
1 Install GracefulShutdown.

Incoming request are resolved as Service Unavailable(503). Optionally you can specify a max amount of time to wait before shutdown:

install(new GracefulShutdown(Duration.ofMinutes(1)));

This extension must be installed at very beginning of your route pipeline.

15.5. HeadHandler

Jooby doesn’t support HTTP HEAD requests by default. To support them you have two options:

  • Use the built-in HeadHandler

  • Write your own head handler

The HeadHandler supports HEAD requests over existing GET handlers.

Head Example
Java
Kotlin
import io.jooby.Jooby;
import io.jooby.HeadHandler;
...
{

  use(new HeadHandler()); (1)

  get("/", ctx -> {
    ...
  });
}
1 Install HeadHandler

HEAD / produces an empty response with a Content-Length header (when possible) and any other header produce it by the GET handler.

The GET handler is executed but produces an empty response.

15.6. RateLimitHandler

Rate limit handler using Bucket4j.

Add the dependency to your project:

Maven
Gradle
<dependency>
  <groupId>com.github.vladimir-bukhtoyarov</groupId>
  <artifactId>bucket4j-core</artifactId>
  <version>8.0.1</version>
</dependency>
10 requests per minute
Java
Kotlin
{
  Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
  Bucket bucket = Bucket4j.builder().addLimit(limit).build();     (1)

  before(new RateLimitHandler(bucket));                           (2)
}
1 Creates a bucket
2 Install the RateLimitHandler
10 requests per minute per ip/remote address
Java
Kotlin
{
  before(new RateLimitHandler(remoteAddress -> {
    Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
    return Bucket4j.builder().addLimit(limit).build();
  }));
}
10 requests per minute per header value
Java
Kotlin
{
  before(new RateLimitHandler(key -> {
    Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
    return Bucket4j.builder().addLimit(limit).build();
  }, "ApiKey"));
}
10 requests per minute
Java
Kotlin
{
  Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
  Bucket bucket = Bucket4j.builder().addLimit(limit).build();     (1)

  before(new RateLimitHandler(bucket));                           (2)
}
1 Creates a bucket
2 Install the RateLimitHandler
Rate limit in a cluster
Java
Kotlin
{
  ProxyManager<String> buckets = ...;
  before(RateLimitHandler.cluster(key -> {
    return buckets.getProxy(key, () -> {
      return ...;
    });
  }));
}

For using it inside a cluster you need to configure one of the bucket4j options for clustering.

15.7. SSLHandler

The SSLHandler forces client to use HTTPS by redirecting non-HTTPS calls to the HTTPS version.

Force SSL
Java
Kotlin
import io.jooby.Jooby;
import io.jooby.SSLHandler;
...
{

  setServerOptions(new ServerOptions().setSecurePort(8443));

  before(new SSLHandler()); (1)

  get("/", ctx -> {
    return ctx.getScheme();
  });
}
1 Install SSLHandler

The SSL Handler recreates the HTTPs URL version using the Host header, if you are behind a proxy you will need to use the X-Forwarded-Host header. To do that set the trust proxy option.

Optionally, you can specify the host to use:

Force SSL
Java
Kotlin
import io.jooby.Jooby;
import io.jooby.SSLHandler;
...
{

  setServerOptions(new ServerOptions().setSecurePort(8443));

  before(new SSLHandler("myhost.org")); (1)

  get("/", ctx -> {
    return ctx.getScheme();
  });
}

For more information about SSL, please check the configure SSL section.

If you run behind a reverse proxy that has been configured to send the X-Forwarded-* header, please consider to add the proxy-peer-address.adoc to your pipeline.

15.8. TraceHandler

Jooby doesn’t support HTTP Trace requests by default. To support them you have two options:

  • Use the built-in TraceHandler

  • Write your own trace handler

The TraceHandler supports TRACE requests over existing handlers.

Head Example
Java
Kotlin
import io.jooby.Jooby;
import io.jooby.TraceHandler;
...
{

  use(new TraceHandler()); (1)

  get("/", ctx -> {
    ...
  });
}
1 Install TraceHandler

TRACE / performs a message loop-back test along the path to the target resource, providing a useful debugging mechanism.

16. Configuration

Application configuration is based on config library. Configuration can by default be provided in either Java properties, JSON, and HOCON files.

Jooby allows overriding any property via system properties or environment variables.

16.1. Environment

The application environment is available via the Environment class, which allows specifying one or many unique environment names.

The active environment names serve the purpose of allowing loading different configuration files depending on the environment. Also, Extension modules might configure application services differently depending on the environment too. For example: turn on/off caches, reload files, etc.

Initializing the Environment
Java
Kotlin
{
  Environment env = getEnvironment();
}

The active environment names property is set in one of this way:

  • As program argument: java -jar myapp.jar application.env=foo,bar; or just java -jar myapp.jar foo,bar

This method works as long you start the application using one of the runApp methods
  • As system property: java -Dapplication.env=foo,bar -jar myapp.jar

  • As environment variable: application.env=foo,bar

The getEnvironment() loads the default environment.

16.2. Default Environment

The default environment is available via loadEnvironment(EnvironmentOptions) method.

This method search for an application.conf file in three location (first-listed are higher priority):

  • ${user.dir}/conf. This is a file system location, useful is you want to externalize configuration (outside of jar file)

  • ${user.dir}. This is a file system location, useful is you want to externalize configuration (outside of jar file)

  • classpath:// (root of classpath). No external configuration, configuration file lives inside the jar file

We use $user.dir to reference System.getProperty("user.dir"). This system property is set by the JVM at application startup time. It represent the current directory from where the JVM was launch it.
File system loading
└── conf
    └── application.conf
└── myapp.jar

A call to:

  Environment env = getEnvironment();

Loads the application.conf from conf directory. You get the same thing if you move the application.conf to myapp.jar directory.

Classpath loading
└── myapp.jar
   └── application.conf (file inside jar)
Jooby favors file system property loading over classpath property loading. So, if there is a property file either in the current directory or conf directory it hides the same file available in the classpath.

16.3. Overrides

Property overrides is done in multiple ways (first-listed are higher priority):

  • Program arguments

  • System properties

  • Environment variables

  • Environment property file

  • Property file

application.conf
foo = foo
Property access
Java
Kotlin
{
  Environment env = getEnvironment();                (1)
  Config conf = env.getConfig();             (2)
  System.out.println(conf.getString("foo")); (3)
}
1 Get environment
2 Get configuration
3 Get foo property and prints foo

At runtime you can override properties using:

Program argument
java -jar myapp.jar foo=argument

Example prints: argument

System property
java -Dfoo=sysprop -jar myapp.jar

Prints: syspro

Environment variable
foo=envar java -jar myapp.jar

Prints: envar

If you have multiple properties to override, it is probably better to collect all them into a new file and use active environment name to select them.

Environment property file
└── application.conf
└── application.prod.conf
application.conf
foo = foo
bar = devbar
application.prod.conf
bar = prodbar
Run with prod environment
java -jar my.app application.env=prod

Or just

java -jar my.app prod
You only need to override the properties that changes between environment not all the properties.

The application.conf defines two properties : foo and bar, while the environment property file defines only bar.

For Multiple environment activation you need to separate them with , (comma):

Run with prod and cloud environment
 java -jar my.app application.env=prod,cloud

16.4. Custom environment

Custom configuration and environment are available too using:

Environment options
Java
Kotlin
{
  Environment env = setEnvironmentOptions(new EnvOptions() (1)
    .setFilename("myapp.conf")
  )
}
1 Load myapp.conf using the loading and precedence mechanism described before

The setEnvironmentOptions(EnvironmentOptions) method loads, set and returns the environment.

To skip/ignore Jooby loading and precedence mechanism, just instantiate and set the environment:

Direct instantiation
Java
Kotlin
{
  Config conf = ConfigFatory.load("/path/to/myapp.conf");  (1)
  Environment env = new Env(customConfig, "prod");         (2)
  setEnvironment(env);                                     (3)
}
1 Loads and parses configuration
2 Create a new environment with configuration and (optionally) active names
3 Set environment on Jooby instance
Custom configuration is very flexible. You can reuse Jooby mechanism or provide your own. The only thing to keep in mind is that environment setting must be done at very early stage, before starting the application.

16.5. Logging

Jooby uses Slf4j for logging which give you some flexibility for choosing the logging framework.

16.5.1. Logback

The Logback is probably the first alternative for Slf4j due its natively implements the SLF4J API. Follow the next steps to use logback in your project:

1) Add dependency

Maven
Gradle
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.5.12</version>
</dependency>
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

That’s all! Slf4j is going to redirect log message to logback.

16.5.2. Log4j2

The Log4j2 project is another good alternative for logging. Follow the next steps to use logback in your project:

1) Add dependencies

Maven
Gradle
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-slf4j-impl</artifactId>
  <version>2.24.1</version>
</dependency>

<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.24.1</version>
</dependency>
log4j.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <Appenders>
    <Console name="stdout">
      <PatternLayout pattern="%d [%t] %-5level: %msg%n%throwable" />
    </Console>
  </Appenders>
  <Loggers>
    <Root level="INFO" additivity="true">
      <AppenderRef ref="stdout" />
    </Root>
  </Loggers>
</Configuration>

All these extensions are considered valid: .xml, .propertines, .yaml and .json. As well as log4j2 for file name.

16.5.3. Environment logging

Logging is integrated with the environment names. So it is possible to have a file name:

  • logback[.name].xml (for loggback)

  • log4j[.name].xml (for log4j2)

Jooby favors the environment specific logging configuration file over regular/normal logging configuration file.

Example
conf
└── logback.conf
└── logback.prod.conf

To use logback.prod.conf, start your application like:

java -jar myapp.jar application.env=prod

The logging configuration file per environment works as long you don’t use static loggers before application has been start it. The next example won’t work:

public class App extends Jooby {
  private static final Logger log = ...

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}

The runApp method is the one who configures the logging framework. Adding a static logger force the logging framework to configure without taking care the environment setup.

There are a couple of solution is for this:

  • use an instance logger

  • use the getLog() method of Jooby

16.6. Application Properties

These are the application properties that Jooby uses:

Property name Description Default value

application.charset

Charset used by your application. Used by template engine, HTTP encoding/decoding, database driver, etc.

UTF-8

application.env

The active environment names. Use to identify dev vs non-dev application deployment. Jooby applies some optimizations for `non-dev`environments

dev

application.lang

The languages your application supports. Used by Context.locale()

A single locale provided by Locale.getDefault().

application.logfile

The logging configuration file your application uses. You don’t need to set this property, see logging configuration.

application.package

The base package of your application.

application.pid

JVM process ID.

The native process ID assigned by the operating system.

application.startupSummary

The level of information logged during startup.

application.tmpdir

Temporary directory used by your application.

tmp

See AvailableSettings for more details.

17. Testing

This section will show you how to run unit and integration tests with Jooby.

17.1. Unit Testing

1) Add Jooby test dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-test</artifactId>
  <version>3.5.0</version>
</dependency>

2) Write your application:

App
Java
Kotlin
public class App extends Jooby {
  {
    get("/", ctx -> "Easy unit testing!");
  }
}

3) Write your test:

TestApp
Java
Kotlin
import io.jooby.test.MockRouter;

public class TestApp {

  @Test
  public void test() {
    MockRouter router = new MockRouter(new App());
    assertEquals("OK", router.get("/").value());
  }
}

Simple and easy ♡!

The MockRouter returns the value produced by the route handler. It is possible to get access and check response metadata:

App
Java
Kotlin
public class App extends Jooby {
  {
    get("/", ctx -> ctx
        .setResponseCode(StatusCode.OK)
        .send("Easy unit testing")
    );
  }
}
Checking response metadata
Java
Kotlin
public class TestApp {

  @Test
  public void test() {
    MockRouter router = new MockRouter(new App());
    router.get("/", response -> {
      assertEquals(StatusCode.OK, response.getStatusCode());
      assertEquals("Easy unit testing", response.value(String.class));
    });
  }
}

For more complex route context interaction or responses, you can pass in a MockContext:

App
Java
Kotlin
public class App extends Jooby {
  {
    post("/", ctx -> {
      String name = ctx.form("name").value();
      return name;
    });
  }
}
Using mock context
Java
Kotlin
public class TestApp {

  @Test
  public void test() {
    MockRouter router = new MockRouter(new App());
    MockContext context = new MockContext();

    context.setForm(Formdata.create(context)
            .put("name", "Test!")
        );
    assertEquals("Test!", router.post("/", context).value());
  }
}

Alternative you can provide your own mock context:

Mockito Context
Java
Kotlin
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class TestApp {

  @Test
  public void test() {
    Value name = mock(Value.class);
    when(name.value()).thenReturn("Test!");

    Context context = mock(Context.class);
    when(context.form("name")).thenReturn(name);

    MockRouter router = new MockRouter(new App());

    assertEquals("Test!", router.post("/", context).value());
  }
}

♡ ♡!

For MVC routes you might prefer to write a unit test using a mock library. No need to use MockRouter, but it is possible too.

17.1.1. Options

Unit testing is simple and easy in Jooby. The MockRouter let you execute the route function, while the MockContext allows you to create an light-weight and mutable context where you can set HTTP parameters, body, headers, etc.

17.2. Integration Testing

Integration tests are supported via JUnit 5 extension mechanism.

1) Add Jooby test dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-test</artifactId>
  <version>3.5.0</version>
</dependency>

2) Write your application:

App
Java
Kotlin
public class App extends Jooby {
  {
    get("/", ctx -> "Easy testing!");
  }
}

3) Write your test:

TestApp
Java
Kotlin
import io.jooby.JoobyTest;

@JoobyTest(App.class)
public class TestApp {

  static OkHttpClient client = new OkHttpClient();

  @Test
  public void test() {
    Request request = new Request.Builder()
        .url("http://localhost:8911")
        .build();

    try (Response response = client.newCall(request).execute()) {
      assertEquals("Easy testing!", response.body().string());
    }
  }
}

The example uses OkHttp client, but you are free to use any other HTTP client.

Simple and easy ♡!

The JoobyTest takes care of start and stop the application.

The default port is: 8911. Application port can be configured directly using the port() method:

@JoobyTest(value = App.class, port = 9999)

If port is set to zero(0) a random port is selected. You can inject the server port in your test like:

Server port injection
@JoobyTest(App.class)
public void test(int serverPort) {

}

@JoobyTest(App.class)
public void anotherTest(int serverPort) {

}

The parameter name must be serverPort and be of type int. This injection let you access to the random port used for the method-level application test.

There is a serverPath value too, which is the entire path to the server:

Server path injection
@JoobyTest(App.class)
public void test(String serverPath) { (1)

}

The serverPath variable contains the entire path: http://localhost:port/contextPath.

Here is the list of available injectable values:

  • int serverPort: Give you the port where the application is listening. This is named type injection (name and type are required).

  • String serverPath: Give you the entire server path where the application is listening. This is named type injection (name and type are required).

  • io.jooby.Environment: Give you access to the application environment. This is a type injection (name no matter).

  • com.typesafe.config.Config: Give you access to the application environment. This is a type injection (name no matter).

  • io.jooby.Jooby: Give you access to the application. This is a type injection (name no matter).

These values can be injected via parameter or instance fields.

The JoobyTest annotation starts the application using the test environment name. You can creates a conf/application.test.conf file to override any other values for testing purpose.

17.2.1. Arguments

Application arguments are supported using a factory method strategy:

App
Java
Kotlin
public class App extends Jooby {
  public App(String argument) {         (1)
    get("/", ctx -> "Easy testing!");
  }
}
1 Application requires a String argument

Write a test:

TestApp
Java
Kotlin
import io.jooby.JoobyTest;

public class TestApp {

  @JoobyTest(value = App.class, factoryMethod = "createApp")         (1)
  public void test() {
    Request request = new Request.Builder()
        .url("http://localhost:8911")
        .build();

    try (Response response = client.newCall(request).execute()) {
      assertEquals("Easy testing!", response.body().string());
    }
  }

  public App createApp() {                                           (2)
    return new App("Argument");                                      (3)
  }
}
1 Specify a factory method: createApp
2 Creates the method: must be public and without arguments
3 Creates your application

If you prefer the annotation at class level (shared application between tests) the factory method must be static.

18. Development

The jooby run tool allows to restart your application on code changes without exiting the JVM.

This feature is also known as hot reload/swap. Makes you feel like coding against a script language where you modify your code and changes are visible immediately.

The tool uses the JBoss Modules library that effectively reload application classes.

For now jooby run is available as Maven and Gradle plugins.

18.1. Usage

1) Add build plugin:

pom.xml
build.gradle
<plugins>
  ...
  <plugin>
    <groupId>io.jooby</groupId>
    <artifactId>jooby-maven-plugin</artifactId>
    <version>3.5.0</version>
  </plugin>
  ...
</plugins>

2) Set main class

pom.xml
build.gradle
<properties>
  <application.class>myapp.App</application.class>
</properties>

3) Run application

Maven
Gradle
mvn jooby:run

18.2. Compilation & Restart

Changing a java or kt file triggers a compilation request. Compilation is executed by Maven/Gradle using an incremental build process.

If compilation succeed, application is restarted.

Compilation errors are printed to the console by Maven/Gradle.

Changing a .conf, .properties file triggers just an application restart request. They don’t trigger a compilation request.

Compiler is enabled by default, except for Eclipse users. Plugin checks for .classpath file in project directory, when found plugin compiler is OFF and let Eclipse compiles the code.

18.3. Options

The next example shows all the available options with their default values:

pom.xml
build.gradle
<plugins>
  ...
  <plugin>
    <groupId>io.jooby</groupId>
    <artifactId>jooby-maven-plugin</artifactId>
    <version>3.5.0</version>
    <configuration>
      <mainClass>${application.class}</mainClass>                  (1)
      <restartExtensions>conf,properties,class</restartExtensions> (2)
      <compileExtensions>java,kt</compileExtensions>               (3)
      <port>8080</port>                                            (4)
      <waitTimeBeforeRestart>500</waitTimeBeforeRestart>           (5)
      <useSingleClassLoader>false</useSingleClassLoader>           (6)
    </configuration>
  </plugin>
  ...
</plugins>
1 Application main class
2 Restart extensions. A change on these files trigger a restart request.
3 Source extensions. A change on these files trigger a compilation request, followed by a restart request.
4 Application port
5 How long to wait after last file change to restart. Default is: 500 milliseconds.
6 Use a single/fat class loader to run your application. This is required on complex project classpath where you start seeing weird reflection errors. This was the default mode in Jooby 2.x. The new model since 3.x uses a modular classloader which improves restart times and memory usage making it faster. Default is: false.

For Maven and Gradle there are two variant mvn jooby:testRun and ./gradlew joobyTestRun they work by expanding the classpath to uses the test scope or source set.

19. Packaging

This section describes some packaging and distribution options.

19.1. Single jar

This is the default deployment option where you create a single jar (a.k.a fat/uber jar) for your application.

The jooby-cli takes care of configures everything for single jar distribution. Next example shows how to do it in case you created your application manually.

Maven
Gradle
<build>
  <plugins>
    ...
    <plugin>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.6.0</version>
        <executions>
          <execution>
            <id>uber-jar</id>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <createDependencyReducedPom>false</createDependencyReducedPom>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>${application.class}</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    ...
  </plugins>
</build>

Maven users:

mvn clean package

Gradle users:

./gradlew shadowJar

19.2. Stork

Stork is packaging, launch and deploy tool for Java apps.

Stork is only available for Maven projects

To configure stork:

1) Creates a src/etc/stork/stork.yml file (file location is important):

stork.yml
# Name of application (make sure it has no spaces)
name: "${project.artifactId}"

# Display name of application (can have spaces)
display_name: "${project.name}"

# Type of launcher (CONSOLE or DAEMON)
type: DAEMON

# Java class to run
main_class: "${application.class}"

domain: "${project.groupId}"

short_description: "${project.artifactId}"

# Platform launchers to generate (WINDOWS, LINUX, MAC_OSX)
# Linux launcher is suitable for Bourne shells (e.g. Linux/BSD)
platforms: [ LINUX ]

# Working directory for app
# RETAIN will not change the working directory
# APP_HOME will change the working directory to the home of the app
# (where it was intalled) before running the main class
working_dir_mode: RETAIN

# Minimum version of java required (system will be searched for acceptable jvm)
min_java_version: "1.8"

# Min/max fixed memory (measured in MB)
min_java_memory: 512
max_java_memory: 512

# Min/max memory by percentage of system
#min_java_memory_pct: 10
#max_java_memory_pct: 20

# Try to create a symbolic link to java executable in <app_home>/run with
# the name of "<app_name>-java" so that commands like "ps" will make it
# easier to find your app
symlink_java: true

2) Configure Maven Tiles plugin:

Maven
<build>
  <plugins>
    <plugin>
        <groupId>io.repaint.maven</groupId>
        <artifactId>tiles-maven-plugin</artifactId>
        <version>2.40</version>
        <extensions>true</extensions>
        <configuration>
          <tiles>
            <tile>io.jooby:jooby-stork:3.5.0</tile>
          </tiles>
        </configuration>
    </plugin>
  </plugins>
</build>

3) Run mvn package

Stork zip file will be available in the target directory.

20. Server

There are three server implementations:

Servers are automatically registered based on their presence on the project classpath.

To use Jetty, add the dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-jetty</artifactId>
  <version>3.5.0</version>
</dependency>

To use Netty, add the dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-netty</artifactId>
  <version>3.5.0</version>
</dependency>

To use Undertow, add the dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-undertow</artifactId>
  <version>3.5.0</version>
</dependency>

Only one server dependency must be available on classpath.

20.1. Manual setup

Servers are automatically loaded using ServiceLoader API. If you need to add it manually:

Server
Java
Kotlin
import io.jooby.netty.NettyServer;

{
  install(new NettyServer());
}

Server might or might not provide way to configure it (depends on Server). For example JettyServer let you customize the ThreadPool, here is an example to setup Jetty with Loom(Virtual Threads):

Jetty Loom
Java
Kotlin
import io.jooby.jetty.JettyServer;

import java.util.concurrent.Executors;

import org.eclipse.jetty.util.thread.QueuedThreadPool;


{
  // Use virtual Thread in Java 19 with --enable-preview
  QueuedThreadPool worker = new QueuedThreadPool();
  worker.setReservedThreads(0);
  worker.setVirtualThreadsExecutor(Executors.newVirtualThreadPerTaskExecutor());

  install(new JettyServer(worker));
}

20.2. Options

Server options are available via ServerOptions class:

Server Options
Java
Kotlin
{
  setServerOptions(new ServerOptions()
      .setBufferSize(16384)
      .setCompressionLevel(6)
      .setPort(8080)
      .setIoThreads(16)
      .setWorkerThreads(64)
      .setGzip(false)
      .setSingleLoop(false)
      .setDefaultHeaders(true)
      .setMaxRequestSize(10485760)
      .setSecurePort(8433)
      .setSsl(SslOptions.selfSigned())
      .setHttpsOnly(false)
      .setHttp2(true)
      .setExpectContinue(true)
  );
}
  • bufferSize: Buffer size used by server for reading/writing data. Default is: 16k.

  • compressionLevel: Gzip support. Set compression level. Value between: 0..9.

  • port: Server HTTP port or 0 for random port. Default is: 8080.

  • ioThreads: Number of IO threads used by the server. Used by Netty and Undertow. Default is: Runtime.getRuntime().availableProcessors() * 2

  • workerThreads: Number of worker (a.k.a application) threads. Default is: ioThreads * 8.

  • gzip: Gzip support. Default is: false. Deprecated in favor of compressionLevel.

  • singleLoop: Indicates if the web server should use a single loop/group for doing IO or not. Netty only.

  • defaultHeaders: Configure server to set the following headers: Date, Content-Type and Server headers.

  • maxRequestSize: Maximum request size in bytes. Request exceeding this value results in 413(REQUEST_ENTITY_TOO_LARGE) response. Default is 10mb.

  • securePort: Enable HTTPS. This option is fully covered in next section.

  • ssl: SSL options with certificate details. This option is fully covered in next section.

  • isHttpsOnly: bind only to HTTPS port, not HTTP. This requires SSL options to be configured.

  • isHttp2: Enable HTTP 2.0.

  • isExpectContinue: Whenever 100-Expect and continue requests are handled by the server. This is off by default, except for Jetty which is always ON.

Server options are available as application configuration properties too:

application.conf
server.bufferSize = 16384
server.compressionLevel = 6
server.port = 8080
server.ioThreads = 16
server.workerThreads = 64
server.gzip = false
server.singleLoop = false
server.defaultHeaders = true
server.maxRequestSize = 10485760
server.securePort = 8443
server.ssl.type = self-signed | PKCS12 | X509
server.httpsOnly = false
server.http2 = true
server.expectContinue = false

20.3. HTTPS Support

Jooby supports HTTPS out of the box. By default HTTPS is disabled and all requests are served using HTTP. Jooby supports two certificate formats:

  • PKCS12 (this is the default format)

  • X.509

The SslOptions class provides options to configure SSL:

  • cert: A PKCS12 or X.509 certificate chain file in PEM format. Most commonly, a .crt file for X509 and .p12 for PKCS12. It can be an absolute path or a classpath resource. Required.

  • key: A PKCS#8 private key file in PEM format. Most commonly a .key file. It can be an absolute path or a classpath resource. Required when using X.509 certificates.

  • password: Password to use. Required when using PKCS12 certificates.

Hello HTTPS
Java
Kotlin
{
  setServerOptions(new ServerOptions()
      .setSecurePort(8443)             (1)
  );
}
1 Set secure port and use a self-signed certificate

Once SSL is enabled application logs print something like:

listening on:
  http://localhost:8080/
  https://localhost:8443/

The self-signed certificate is useful for development but keep in mind it will generate a warning on the browser.

Not Secure

A better option for development is the mkcert tool:

Generates a PKCS12 certificate
mkcert -pkcs12 localhost
Generates a X.509 certificate
mkcert localhost

20.3.1. Using X.509

To use a valid X.509 certificate, for example one created with Let’s Encrypt. You will need the .crt and .key files:

X509
Java
Kotlin
{
  SslOptions ssl = SslOptions.x509("path/to/server.crt", "path/to/server.key");
  setServerOptions(new ServerOptions()
      .setSsl(ssl)                                                    (1)
  );
}
1 Creates a SslOptions using X509 certificates path

Certificate (.crt) and private key (.key) location can be file system or class path locations.

Optionally you can define the SSL options in your application configuration file:

Ssl options:
server {
  ssl {
    type: X509,
    cert: "path/to/server.crt",
    key: "path/to/server.key"
  }
}
X509 from configuration
Java
Kotlin
{
  setServerOptions(new ServerOptions()
      .setSsl(SslOptions.from(getConfig()))
  );
}

20.3.2. Using PKCS12

To use a valid PKCS12 certificate:

PKCS12
Java
Kotlin
{
  SslOptions ssl = SslOptions.pkcs12("path/to/server.p12", "password");
  setServerOptions(new ServerOptions()
      .setSsl(ssl)                                                      (1)
  );
}
1 Creates SslOptions using PKCS12 certificates path

Certificate (.p12 location can be file system or class path locations.

Optionally you can define the SSL options in your application configuration file:

Ssl options:
server {
  ssl {
    type: PKCS12,
    cert: "path/to/server.p12",
    password: "password"
  }
}
PKCS12 from configuration
Java
Kotlin
{
  setServerOptions(new ServerOptions()
      .setSsl(SslOptions.from(getConfig()))
  );
}

20.3.3. Client Authentication (Mutual TLS)

To enable 2-way TLS (Mutual TLS), set the trust certificate and client authentication. Setting the trust certificate is required if using self-signed or custom generated certificates so that the server will trust the client’s certificate signing authority.

Client Authentication
Java
Kotlin
{
  SslOptions ssl = SslOptions.pkcs12("path/to/server.p12", "password")
    .setTrustCert(Files.newInputStream("path/to/trustCert")) (1)
    .setTrustPassword("password") (2)
    .setClientAuth(SslOptions.ClientAuth.REQUIRED); (3)

  setServerOptions(new ServerOptions()
      .setSsl(ssl)
  );
}
1 Set the trust certificate path.
2 Set the trust certificate password.
3 Set the client authentication mode. Possible values are REQUIRED, REQUESTED, or NONE. Default is NONE.

Optionally you can define these SSL options in your application configuration file:

Ssl options:
server {
  ssl {
    type: PKCS12,
    cert: "path/to/server.p12",
    password: "password",
    trust {
      cert: "path/to/trustCert",
      password: "password"
    }
    clientAuth: REQUIRED
  }
}
Mutual TLS from configuration
Java
Kotlin
{
  setServerOptions(new ServerOptions()
      .setSsl(SslOptions.from(getConfig()))
  );
}

20.3.4. TLS protocol

Default protocol is TLSv1.3, TLSv1.2. To override, just do:

TLS example
Java
Kotlin
{
  setServerOptions(new ServerOptions()
      .setSsl(new SslOptions().setProtocol("TLSv1.3", "TLSv1.2"))
  );
}

If a listed protocol is not supported, it is ignored; however, if you specify a list of protocols, none of which are supported, an exception will be thrown.

TLSv1.3 protocol is available in

  • Open SSL via Conscrypt (see next section)

  • 8u261-b12 from Oracle JDK

  • TLS 1.3 support in OpenJDK is (beside Azul’s OpenJSSE) expected to come into 8u272.

  • Java 11.0.3 or higher.

20.3.5. OpenSSL

SSL support is provided using built-in JDK capabilities. Jooby offers an OpenSSL support using Conscrypt.

To enable, just add the required dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-conscrypt</artifactId>
  <version>3.5.0</version>
</dependency>

Conscrypt is a Java Security Provider (JSP) that implements parts of the Java Cryptography Extension (JCE) and Java Secure Socket Extension (JSSE). It uses BoringSSL to provide cryptographic primitives and Transport Layer Security (TLS) for Java applications on Android and OpenJDK.

20.4. HTTP/2 Support

HTTP2 support is provided across web server implementation. You need to enabled http2 option programmatically or via application.conf properties.

HTTP/2
Java
Kotlin
{
  setServerOptions(new ServerOptions()
      .setHttp2(true)
      .setSecurePort(8433)
  );

  get("/", ctx -> {
    ctx.getProtocol()
  })
}

To use HTTP/2 from browsers you need TLS (the h2 protocol) please refer to HTTPS support to configure TLS.

There is no support for HTTP/2 Push.

21. Extensions and Services

Jooby comes with a simple extension mechanism. The Extension API allows to configure , extend an application by adding shared/single services, infrastructure/technical concerns like dependency injection, database connection pools, cron-job, etc.

Services are shared/singleton objects, usually with a clear lifecycle for starting and stopping them.

21.1. Writing Custom Extension

We are going to develop a custom extension that configure a DataSource service.

Java
Kotlin
import io.jooby.Extension;

public class MyExtension implements Extension {

   public void install(Jooby application) {
      DataSource dataSource = createDataSource();           (1)

      ServiceRegistry registry = application.getServices(); (2)
      registry.put(DataSource.class, dataSource);           (3)

      application.onStop(dataSource::close)                 (4)
   }
}
1 Create the service
2 Access to service registry
3 Add it to the service registry
4 Close/release service on application stop

Let’s install the extension and use the service!!

Java
Kotlin
public class App extends Jooby {

   {
     install(new MyExtension());                      (1)

     get("/", ctx -> {
       MyDataSource ds = require(MyDataSource.class); (2)
       // ...
     });
   }
}
1 Install the extension
2 Use the service

Services are accessible via require(Class).

In addition to services, an extension module may provides infrastructure routes, body decoder/encoder, template engines, etc.

The extension mechanism is a simple way of reusing code and decoupling technical features from business logic.

More advanced techniques are describe in the Dependency Injection section.

22. Dependency Injection

22.1. Avaje Inject

1) Add Avaje Inject to your project

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-avaje-inject</artifactId>
  <version>3.5.0</version>
</dependency>

2) Configure annotation processor

Maven
Gradle
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>...</version>
      <configuration>
        <annotationProcessorPaths>
          <!-- if using lombok, it must be placed before the avaje-inject-generator -->
          <!-- avaje-inject-generator must be placed before the jooby-apt -->
          <path>
            <groupId>io.avaje</groupId>
            <artifactId>avaje-inject-generator</artifactId>
            <version>10.4</version>
          </path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
  </plugins>
</build>

Please note that the order of annotation processors is important. For example, if you’re using lombok and avaje-inject, the correct order should be: lombokavaje-injectjooby-apt

3) Install Avaje Inject:

Installing Avaje Inject
Java
Kotlin
public class App extends Jooby {

  {
    install(AvajeInjectModule.of());                     (1)

    get("/", ctx -> {
      MyService service = require(MyService.class); (2)
      return service.doSomething();
    });
}

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}
1 Install Avaje Inject module
2 The require(Class) call is now resolved by Avaje Inject

22.1.1. Property Injection

Configuration properties can be injected using the @Named annotation. As Avaje checks beans at compile time, @External is required to prevent false-positive compilation errors:

application.conf
currency = USD
Java
Kotlin
@Singleton
public class BillingService {

  @Inject
  public BillingService(@External @Named("currency") String currency) {
    ...
  }

}

22.1.2. MVC routes

Avaje Inject will also provisioning MVC routes

MVC and Avaje Inject
Java
Kotlin
public class App extends Jooby {

  {
    install(AvajeInjectModule.of());  (1)

    mvc(MyController.class);          (2)
  }

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}
1 Install Avaje Inject module
2 Register a MVC route

The lifecycle of MyController is now managed by Avaje Inject.

In Avaje Inject, the dependency graph is typically validated when the application compiles. As beans provided by Jooby Modules are registered at runtime, you must add @External when injecting these runtime beans into @Singleton classes to inform the avaje processor that these beans are provided at runtime.

22.2. Dagger

1) Add Dagger to your project

Maven
Gradle
<dependency>
  <groupId>com.google.dagger</groupId>
  <artifactId>dagger</artifactId>
  <version>2.20</version>
</dependency>

2) Configure annotation processor

Maven
Gradle
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>...</version>
      <configuration>
        <annotationProcessorPaths>
          <path>
            <groupId>com.google.dagger</groupId>
            <artifactId>dagger-compiler</artifactId>
            <version>2.20</version>
          </path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
  </plugins>
</build>

3) Bootstrap Dagger from application:

Dagger
Java
Kotlin
import static io.jooby.Jooby.runApp;

public class App extends Jooby {

  {
     /** Dagger: */
     AppComponent dagger = DaggerAppComponent.builder()      (1)
         .build();

     get("/", ctx -> {
       MyService service = dagger.getMyService();            (2)
       return service.doSomething();
     });
  }

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}
1 Bootstrap dagger component
2 Use dagger provided objects

DaggerAppComponent in the example above is a generated source. Check out the Dagger tutorial to learn more.

22.2.1. MVC routes

Integration of MVC routes with Dagger is as simple as:

MVC and Dagger
Java
Kotlin
import static io.jooby.Jooby.runApp;

public class App extends Jooby {

  {
    /** Dagger: */
    AppComponent dagger = DaggerAppComponent.builder()  (1)
        .build();

    mvc(dagger.myController());                         (2)
  }

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}
1 Bootstrap dagger component
2 Register MVC route provided by Dagger

Due the static nature of Dagger mvc integration identical to normal usage. For custom scopes/lifecycles Dagger generate a jakarta.inject.Provider on such use cases you need to switch and use the provider version of the mvc method:

MVC and Dagger provider
Java
Kotlin
import static io.jooby.Jooby.runApp;

public class App extends Jooby {

  {
    /** Dagger: */
    AppComponent dagger = DaggerAppComponent.builder()      (1)
        .build();

    mvc(MyController.class, dagger.myController());         (2)
  }

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}
1 Bootstrap dagger component
2 Register MVC route using a Dagger provider

22.3. Guice

1) Add Guice dependency to your project:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-guice</artifactId>
  <version>3.5.0</version>
</dependency>

2) Install Guice:

Installing Guice
Java
Kotlin
import io.jooby.guice.GuiceModule;
import io.jooby.kt.runApp;

public class App extends Jooby {

  {
    install(new GuiceModule());                     (1)

    get("/", ctx -> {
      MyService service = require(MyService.class); (2)
      return service.doSomething();
    });
}

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}
1 Install Guice module
2 The require(Class) call is now resolved by Guice

22.3.1. Property Injection

Configuration properties can be injected using the @Named annotation:

application.conf
currency = USD
Java
Kotlin
import javax.injext.Named;
import javax.injext.Inject;

public class BillingService {

  @Inject
  public BillingService(@Named("currency") String currency) {
    ...
  }

}

22.3.2. MVC routes

Guice will also provisioning MVC routes

MVC and Guice
Java
Kotlin
import io.jooby.guice.GuiceModule;
import io.jooby.kt.runApp

public class App extends Jooby {

  {
    install(new GuiceModule());  (1)

    mvc(MyController.class);     (2)
  }

  public static void main(String[] args) {
    runApp(args, App::new);
  }
}
1 Install Guice module
2 Register a MVC route

The lifecycle of MyController is now managed by Guice. Also:

  • In Guice, the default scope is prototype (creates a new instance per request)

  • If you prefer a single instance add the jakarta.inject.Singleton annotation

23. Modules

Modules are a key concept for building reusable and configurable pieces of software.

Modules (unlike in other frameworks) are thin and do a lot of work to bootstrap and configure an external library, but they DO NOT provide a new level of abstraction nor [do] they provide a custom API to access functionality in that library. Instead they expose the library components as they are.

Modules are distributed as separated jar/dependency and usually implement the Extension API.

In general they provide a builder class to create the and configure the external library from configuration properties.

Available modules are listed next.

23.1. Cloud

23.2. Data

23.3. Validation

23.4. Development Tools

  • Jooby Run: Run and hot reload your application.

  • Whoops: Pretty page stacktrace reporter.

  • Metrics: Application metrics from the excellent metrics library.

23.5. Event Bus

  • Camel: Camel module for Jooby.

23.6. JSON

  • Gson: Gson module for Jooby.

  • Jackson: Jackson module for Jooby.

  • JSON-B: JSON-B module for Jooby.

  • Avaje-JsonB: Avaje-JsonB module for Jooby.

23.7. OpenAPI

23.8. Template Engine

23.9. Security

  • Jasypt: Encrypted configuration files.

  • Pac4j: Security engine module.

23.10. Session Store

  • Caffeine: In-memory session store using Caffeine cache.

  • JWT: JSON Web Token session store.

  • Redis: Save session data on redis.

23.11. Scheduler

.

24. Appendix

24.1. Upgrading from 2.x to 3.x

You will find here notes/tips about how to migrate from 2.x to 3.x.

This is a work in progress document, if something is wrong or missing please report to Github or better edit this file and fix it

24.1.1. Requirements

  • Java 17 as minimum

24.1.2. module-info.java

Jooby is now compatible with Java Module system.

Almost all Jooby components are now Java Modules, but not all them. For those where wasn’t possible the Jooby module contains the Automatic-Module-Name manifest entry.

24.1.3. Kotlin

Kotlin was removed from core, you need to the jooby-kotlin dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-kotlin</artifactId>
  <version>3.5.0</version>
</dependency>

24.1.4. jakarta

2.x

3.x

javax.servlet

jakarta.servlet

javax.inject

jakarta.inject

javax.persistence

jakarta.persistence

24.1.5. Modules

2.x

3.x

jooby-kotlin

Added

jooby-weld

Removed

jooby-archetype

Removed

jooby-utow

Renamed: jooby-undertow

jooby-commons-email

Renamed: jooby-commons-mail

jooby-http2-jetty

Merged into: jooby-netty

jooby-http2-netty

Merged into: jooby-netty

jooby-http2-undertow

Merged into: jooby-undertow

24.1.6. Package renames

2.x

3.x

Module

io.jooby.annotations

io.jooby.annotation

jooby (core)

io.jooby

io.jooby.test

jooby-test

io.jooby (Kotlin)

io.jooby.kt

removed from jooby, now in jooby-kotlin

io.jooby.graphql

io.jooby.graphiql

jooby-graphiql

io.jooby.graphql

io.jooby.graphql.playground

jooby-graphql-playground

io.jooby.json

io.jooby.gson

jooby-gson

io.jooby.json

io.jooby.jackson

jooby-jackson

io.jooby.di

io.jooby.guice

jooby-guice

io.jooby.di

io.jooby.spring

jooby-spring

io.jooby.aws

io.jooby.awssdkv1

jooby-awssdk-v1

io.jooby.email

io.jooby.commons.mail

jooby-commons-mail

io.jooby.utow

io.jooby.undertow

jooby-undertow

24.1.7. Class renames

2.x

3.x

Module

io.jooby.Route.Decorator

io.jooby.Route.Filter

jooby (core)

io.jooby.Kooby

io.jooby.kt.Kooby

jooby-kotlin (new module)

io.jooby.jetty.Jetty

io.jooby.jetty.JettyServer

jooby-jetty

io.jooby.netty.Netty

io.jooby.netty.NettyServer

jooby-netty

io.jooby.utow.Utow

io.jooby.undertow.UndertowServer

jooby-undertow

io.jooby.AccessLogHandler

io.jooby.handler.AccessLogHandler

jooby (core)

io.jooby.Cors

io.jooby.handler.Cors

jooby (core)

io.jooby.CorsHandler

io.jooby.handler.CorsHandler

jooby (core)

io.jooby.CsrfHandler

io.jooby.handler.CsrfHandler

jooby (core)

io.jooby.HeadHandler

io.jooby.handler.HeadHandler

jooby (core)

io.jooby.RateLimitHandler

io.jooby.handler.RateLimitHandler

jooby (core)

io.jooby.SSLHandler

io.jooby.handler.SSLHandler

jooby (core)

io.jooby.TraceHandler

io.jooby.handler.TraceHandler

jooby (core)

io.jooby.WebVariables

io.jooby.handler.WebVariables

jooby (core)

io.jooby.Asset

io.jooby.handler.Asset

jooby (core)

io.jooby.AssetHandler

io.jooby.handler.AssetHandler

jooby (core)

io.jooby.AssetSource

io.jooby.handler.AssetSource

jooby (core)

io.jooby.CacheControl

io.jooby.handler.CacheControl

jooby (core)

24.1.8. Method renames

2.x

3.x

Description

Router.decorator(Decorator)

Router.use(Filter)

decorator has been deprecated in favor of use

SslOptions.setCert(String)

SslOptions.setCert(InputStream)

Replaced the string argument with InputStream

SslOptions.setTrustCert(String)

SslOptions.setTrustCert(InputStream)

Replaced the string argument with InputStream

SslOptions.setPrivateKey(String)

SslOptions.setPrivateKey(InputStream)

Replaced the string argument with InputStream

FileUpload.destroy

FileUpload.close

destroy has been replaced by close

Context.attribute(String)

Context.getAttribute(String)

Context.attribute(String, Object)

Context.setAttribute(String, Object)

Context.multipart*

Context.form*

All Context.multipart related methods where merged into Context.form

Context.query<Type>()

Context.query(Type::class)

Kotlin

Context.form<Type>()

Context.form(Type::class)

Kotlin

Context.body<Type>()

Context.body(Type::class)

Kotlin

24.1.9. Dependencies

2.x

3.x

Module

Slf4j 1.x

Slf4j 2.x

jooby (core)

Jetty 9.x

Jetty 11.x

jooby-jetty

Guice 5.x

Guice 7.x

jooby-guice

24.1.10. Reactive support

Reactive libraries has been removed from core to his own module.

2.x

3.x

rxjava

jooby-rxjava3

reactor

jooby-reactor

All reactive libraries requires explicit handler while using script/lambda routes. More details on NonBlocking responses.

24.2. Upgrading from 1.x to 2.x

You will find here notes/tips about how to migrate from 1.x to 2.x.

24.2.1. Maven coordinates

org.jooby became io.jooby. Hence, use <groupId>org.jooby</groupId> for all dependencies.

24.2.2. Modules

1.x

2.x

jooby-apitool

no real equivalent, use a combination of jooby-maven-plugin and jooby-swagger-ui

jooby-hbv

n/a

jooby-lang-kotlin

not needed anymore, part of core now

jooby-servlet

n/a

24.2.3. API

API still similar/equivalent in 2.x. Except for the one listed below:

Table 1. Classes

1.x

2.x

org.jooby.Module

io.jooby.Extension

org.jooby.Env

io.jooby.Environment

org.jooby.Mutant

io.jooby.Value

org.jooby.Render

io.jooby.MessageEncoder

org.jooby.Parser

io.jooby.MessageDecoder

org.jooby.Err

io.jooby.StatusCodeException

org.jooby.Results

- (removed)

org.jooby.Result

- (removed)

24.2.4. Route Pipeline

The concept of route pipeline still applies for 2.x but works different.

In 1.x there is no difference between handler and filter (including before and after). The way to chain multiple handler/filter was like:

Pipeline in 1.x
{
  use("*", (req, rsp, chain) -> {
    System.out.println("first");
    // Moves execution to next handler: second
    chain.next(req, rsp);
  });

  use("*", (req, rsp, chain) -> {
    System.out.println("second");
    // Moves execution to next handler: third
    chain.next(req, rsp);
  });

  get("/handler", req -> {
    return "third";
  });
}

A filter in 1.x requires a path pattern, here we use a wide matcher * for first and second filters. Both of this filters are going to be executed before the real handler.

Pipeline in 2.x
{
   use(next -> ctx -> {
     System.out.println("first");
     // Moves execution to next handler: second
     return next.apply(ctx);
   });

   use(next -> ctx -> {
     System.out.println("second");
     // Moves execution to next handler: third
     return next.apply(ctx);
   });

   get("/handler", ctx -> {
     return "third";
   });
}

Execution is identical to 1.x. The first and second decorators are executed before the handler. Differences with 1.x are:

  • Route.Decorator doesn’t support a path pattern. In 1.x the path pattern is required for a filter.

  • Only the handler supports a path pattern and HTTP-method.

  • A handler might have zero or more decorator.

  • In 2.x we chain all the decorator defined before the handler.

The routing matching algorithm in 2.x is more efficient and fast, because:

  • Matches a single path pattern (due decorator lacks of path pattern)

  • Uses a radix tree, not regular expression like in 1.x

  • It never executes a decorator if there isn’t a matching handler

More detailed explanation of route pipeline is available in the router pipeline documentation.