Jooby

∞ do more, more easily

Jooby is a modular, high-performance web framework for Java and Kotlin. Designed for simplicity and speed, it gives you the freedom to build on your favorite server with a clean, modern API.

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);
  }
}

Features

Tip

Latest Release: 4.0.16.

Looking for a previous version?

Getting Started

Introduction

Script API

The Script API (also known as script routes) provides a fluent, reflection-free DSL based on lambda functions.

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

Extending Jooby (Recommended for Java):
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 to keep the DSL clean (avoiding the need to prefix methods like get with a variable name). However, you can also define routes without subclassing:

Without extending Jooby:
Java
Kotlin
import io.jooby.Jooby;

public class App {
  public static void main(String[] args) {
    Jooby.runApp(args, app -> {
      app.get("/", ctx -> "Hello Jooby!");
    });
  }
}

(Note: For Kotlin, the DSL remains clean whether you extend Kooby or not).

MVC API

The MVC API uses annotations to define routes and bytecode generation to execute them quickly.

MVC Routing:
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) {
    Jooby.runApp(args, app -> {
      app.use(new MyController_());
    });
  }
}

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

Quick Start

The fastest way to start building is with the jooby console. This lightweight CLI generates configured Jooby projects instantly.

CLI Features:

  • Scaffolds Maven or Gradle builds.

  • Generates Java or Kotlin applications.

  • Configures Script or MVC routes.

  • Selects Jetty, Netty, or Undertow as the underlying server.

  • Prepares Uber/Fat jars or Stork native launchers.

  • Generates a ready-to-use Dockerfile.

Installing the CLI

  1. Download jooby-cli.zip.

  2. Unzip the file into a preferred directory.

  3. Locate the native launchers in the bin folder.

Tip

Add the launcher (bin/jooby on Linux/macOS, or bin/jooby.bat on Windows) to your system’s PATH to run it from any directory.

Note

The jooby CLI requires Java 17 or higher. Windows users should use jooby.bat.

Creating a Project

First, set your workspace directory where new projects will be saved:

jooby set -w ~/Source

Next, open the console by typing jooby and pressing ENTER.

To see all available options, type help create:

jooby> help create
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 an MVC application
  -s, --stork             Add Stork Maven plugin to build (Maven only)
      --server=<server>   Choose a server: jetty, netty, or undertow

Examples:

Create a default Maven Java project:
jooby> create myapp
Create a Kotlin project:
jooby> create myapp --kotlin
Note
Kotlin Dependency

Since version 3.x, Kotlin is no longer included in the core. The CLI will automatically add the required dependency:

Create a Gradle project (Java or Kotlin):
jooby> create myapp --gradle
jooby> create myapp --gradle --kotlin
Create an MVC-based project:
jooby> create myapp --mvc
Specify the server (default is Netty):
jooby> create myapp --server undertow
Include Dockerfile and Stork launcher configurations:
jooby> create myapp --docker --stork

For full control over the groupId, package structure, and versioning, use interactive mode:

jooby> create myapp -i

Understanding Code Snippets

Throughout this documentation, we prioritize brevity. Unless strictly necessary, code examples will omit the main method and class definitions.

When you see a snippet like this:

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

Assume it is taking place inside the Jooby initializer block or the runApp function.

Core

The heart of the Jooby development experience. This section defines the Request-Response Pipeline, covering everything from expressive routing and path patterns to managing the Context and crafting fluid responses. It is the essential guide to building the logic that powers your web applications.

Router

The Router is the heart of Jooby and consists of:

  • A routing algorithm (radix tree)

  • One or more routes

  • A collection of operators over routes

Route

A Route consists of three parts:

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 (e.g., GET, POST)

  2. Path pattern (e.g., /foo, /foo/{id})

  3. Handler function

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

Attributes

Attributes allow you to annotate a route at application bootstrap time. They function as static metadata available at runtime:

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

An attribute consists of a name and a value. Values can be any object. Attributes can be accessed during the 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().getAttribute("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().getAttribute("Role"));
  });
}

The previous example prints admin. You can retrieve all attributes of a route by calling ctx.getRoute().getAttributes().

Any runtime annotation is automatically added as a route attribute following these rules:

  • If the annotation has a value method, the annotation’s name becomes the attribute name.

  • Otherwise, the method name is used as the attribute name.

Path Pattern

Static
Java
Koltin
{
  get("/foo", ctx -> "Foo");
}
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. Retrieves the variable id as an 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. Retrieves the string variable file.

  3. Retrieves the 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 ? makes it optional.

  2. Retrieves the variable id as a String if present, or uses the default value: self.

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

  • /profile

  • /profile/eespina

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. The regex expression is everything after the first : (e.g., [0-9]+).

  2. Retrieves the int value.

The optional syntax is also supported for regex path variables (e.g., /user/{id:[0-9]+}?). This matches:

  • /user

  • /user/123

* 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 the catchall value using the * character.

  3. In this example, we named the catchall pattern and access it using the path variable name.

Note

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

Handler

Application logic belongs 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.

Note

An incoming request matches exactly ONE route handler. If no handler matches, it 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 ⇒ overridden by the next route.

  4. GET /usersnew users (not users).

Routes with a more specific path pattern (2 vs 1) have higher precedence. Also, if multiple routes share the same path pattern (like 3 and 4), the last registered route overrides the previous ones.

Filter

Cross-cutting concerns such as response modification, verification, security, and tracing are 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 the start time.

  2. Proceeds with execution (the pipeline).

  3. Computes and prints latency.

  4. Returns a response.

Note

One or more filters applied on top of a handler produce a new handler.

Before

The before filter runs before a handler.

A before filter takes a context as an argument and does not produce a response. It is expected to operate via side effects (usually modifying the HTTP request/response).

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

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

The after filter runs after a handler.

An after filter takes three arguments. The first is the HTTP context, the second is the result from a functional handler (or null for a side-effect handler), and the third is any exception generated by the handler.

It is expected to operate via side effects, usually modifying the HTTP response (if possible) or cleaning up/tracing 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. Adds a response header (modifies the HTTP response).

If the target handler is a functional handler, modifying the HTTP response is allowed.

For side-effect handlers, the after filter is invoked with a null result and is not 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.

An exception occurs here because the response was already started, and it is impossible to alter it. Side-effect handlers are those that use the send methods, responseOutputStream, or responseWriter.

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

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

An after handler is always invoked.

The next examples demonstrate some use cases for dealing with errored responses. Keep in mind that an after handler is not a mechanism for handling and reporting exceptions; that is the task of an Error Handler.

Run code depending on 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, giving the Error Handler a chance to jump in.

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

Here, the exception won’t be propagated because we produce a response, meaning the error handler will not execute.

If the after handler produces a new exception, that exception will be added to the original exception as a 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.

Complete

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

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

Example
Java
Kotlin
{
   use(next -> ctx -> {
     long start = System.currentTimeMillis();
     ctx.onComplete(context -> {                       // 1
       long end = System.currentTimeMillis();          // 2
       System.out.println("Took: " + (end - start));
     });
     return next.apply(ctx);
   });
}
  1. Attaches a completion listener.

  2. Runs after the response has been fully written.

Completion listeners are invoked in reverse order.

Pipeline

The route pipeline (a.k.a. route stack) is a composition of one or more use statements 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 scenes, Jooby builds something like this:

{
  // 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 chained into a new handler.

Note
Filter without path pattern

This was a hard decision, but it is the right one. Jooby 1.x used a path pattern to define a filter.

In Jooby 1.x, the pipeline consisted of multiple filters and handlers matched sequentially. The following filter was always executed:

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

   // ...
}

If a bot tried to access missing pages (causing `404`s), Jooby 1.x executed the filter for every single request before realizing there was no matching route.

In Jooby 2.x+, this no longer happens. The pipeline is only executed if there is a matching handler. Otherwise, nothing is executed!

Order

Order follows a what you see is what you get approach. Routes are stacked in the order they are 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

Scoped Filter

The routes(Runnable) and path(String,Runnable) operators group one or more routes.

A scoped filter looks like this:

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. Introduces a new scope via the routes operator.

  2. /49

  3. /12

It functions as a normal filter inside a group operator.

Grouping Routes

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

Route operator
Java
Kotlin
{
  routes(() -> {
    get("/", ctx -> "Hello");
  });
}

The routes operator is for grouping routes and applying cross-cutting concerns to all of them.

Similarly, the path(String,Runnable) operator groups 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. Sets the common prefix /api/user.

  2. GET /api/user/{id}

  3. GET /api/user

  4. POST /api/user

Composing

Mount

Composition is a technique for building modular applications. You can compose one or more routers into a new one using 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. Adds 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 imports routes. Services and callbacks are not imported. The main application is responsible for assembling all resources and services required by the imported applications.

Install

Alternatively, you can install a standalone application into another one using the install(Supplier) 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, deploy Foo as a standalone application or integrate it into a main application.

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 lazily initialized:

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

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 need to support both the old and new versions concurrently based on a header:

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! ♡

Multiple Routers

This model lets you run multiple applications on a single server instance. Each application works like a standalone application; they do not share any services.

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

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

import static io.jooby.Jooby.runApp;

public class MultiApp {
  public static void main(String[] args) {
    runApp(args, List.of(Foo::new, Bar::new));
  }
}

You write your application as usual and then deploy them using the runApp method.

Important

Due to the nature of logging frameworks (static loading and initialization), logging bootstrap might not work as expected. It is recommended to just use the logback.xml or log4j2.xml file.

Options

Routing

The setRouterOptions(RouterOptions) method controls routing behavior.

Usage
Java
Kotlin
import io.jooby.Jooby;
...
{
  setRouterOptions(new RouterOptions()
      .setIgnoreCase(true)
      .setFailOnDuplicateRoutes(true)
      .setIgnoreTrailingSlash(true)
      .setNormalizeSlash(true)
      .setResetHeadersOnError(false)
      .setTrustProxy(true)
      .setContextAsService(true)
  );
}
Option Type Default Description

contextAsService

boolean

false

If enabled, allows you to retrieve the Context object associated with the current request via the service registry while the request is being processed.

ignoreCase

boolean

false

Indicates whether the routing algorithm uses case-sensitive matching for incoming request paths.

ignoreTrailingSlash

boolean

false

Indicates whether trailing slashes are ignored on incoming request paths.

failOnDuplicateRoutes

boolean

false

Throws an exception if multiple routes are registered with the same HTTP method and path pattern.

normalizeSlash

boolean

false

Normalizes incoming request paths by replacing multiple consecutive slashes with a single slash.

resetHeadersOnError

boolean

true

Indicates whether response headers should be cleared/reset when an exception occurs.

trustProxy

boolean

false

When true, parses X-Forwarded-* headers and updates the current context to reflect the values sent by the proxy.

Important

trustProxy: This should only be enabled if your application is running behind a reverse proxy configured to send X-Forwarded-* headers. Otherwise, remote users can spoof their IP addresses and protocols by sending malicious headers.

Hidden Method

The setHiddenMethod(String) option allows clients to override the HTTP method. This is especially useful for HTML forms, which natively only support GET and POST.

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
    return "Updated!";
  });
}
  1. Configures the router to look for a form field named _method.

  2. Executes the PUT handler for /form instead of the standard POST.

(Note: I fixed a small bug in your Kotlin snippet where you were using Java lambda syntax inside the put route).

The default implementation looks for the specified hidden field in POST forms or multipart requests. Alternatively, you can provide a custom strategy, such as reading an HTTP header:

HTTP Header Strategy
Java
Kotlin
import io.jooby.Jooby;
...
{
  setHiddenMethod(ctx -> ctx.header("X-HTTP-Method-Override").toOptional());  // 1
}
  1. Overrides the HTTP method by extracting it from the X-HTTP-Method-Override request header.

Context

A Context allows you to interact with the HTTP request and manipulate the HTTP response.

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

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

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

These methods use Locale.lookup(…​) and Locale.filter(…​) to perform language tag matching. (See their overloads if you need to plug in a custom matching strategy).

To leverage language matching, you must tell Jooby which languages your application supports. Set the application.lang configuration property to a value compatible with the Accept-Language header:

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

Or configure it programmatically using setLocales(List):

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

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

Parameters

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

This section covers how to extract raw parameters. The next section covers how to convert them into complex objects using the Value API.

Header

HTTP headers allow the client and server to pass additional information.

Java
Kotlin
{
  get("/", ctx -> {
    String token = ctx.header("token").value();       // 1
    Value headers = ctx.headers();                    // 2
    Map<String, String> headerMap = ctx.headerMap();  // 3
    // ...
  });
}
  1. Retrieves the header token.

  2. Retrieves all headers as a Value.

  3. Retrieves all headers as a Map.

Request cookies are sent to the server via the Cookie header. Jooby provides simple key/value access:

Cookies
Java
Kotlin
{
  get("/", ctx -> {
    String token = ctx.cookie("token").value();       // 1
    Map<String, String> cookieMap = ctx.cookieMap();  // 2
    // ...
  });
}
  1. Retrieves the cookie named token.

  2. Retrieves all cookies as a Map.

Path

Path parameters are part of the URI. Use the {identifier} notation to define a path variable.

Syntax:
Java
Kotlin
{
  get("/{id}", ctx -> ctx.path("id").value());                                  // 1
  get("/@{id}", ctx -> ctx.path("id").value());                                 // 2
  get("/file/{name}.{ext}", ctx -> ctx.path("name") + "." + ctx.path("ext"));   // 3
  get("/file/*", ctx -> ctx.path("*"));                                         // 4
  get("/{id:[0-9]+}", ctx -> ctx.path("id"));                                   // 5
}
  1. Standard path variable id.

  2. Path variable id prefixed with @.

  3. Multiple variables: name and ext.

  4. Unnamed catchall path variable.

  5. Path variable strictly matching a regular expression.

Accessing Path Variables
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 the raw path string (e.g., /a b returns /a%20b).

  2. Path as a Value object (decoded).

  3. Path as a Map<String, String> (decoded).

  4. Specific path variable name as a String (decoded).

Query

The query string is the part of the URI that starts 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 the raw query string (e.g., ?q=a%20b).

  2. Query string as a QueryString object (e.g., {q=a b}).

  3. Query string as a multi-value map (e.g., {q=[a b]}).

  4. Access decoded variable q. Throws a 400 Bad Request if missing.

  5. Binds the query string directly to a SearchQuery object.

Formdata

Form data is sent in the HTTP body (or as part of the URI for GET requests) and is 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;
   }
}
  1. Form as Formdata.

  2. Form as a multi-value map.

  3. Specific form variable id.

  4. Specific form variable pass.

  5. Form automatically bound to a User object.

Multipart & File Uploads

Multipart data is sent in the HTTP body and encoded as multipart/form-data. It is required for file uploads.

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;
   }
}
  1. Form as Multipart.

  2. Form as a multi-value map.

  3. Specific multipart text variable id.

  4. Specific multipart text variable pass.

  5. Single file upload named pic.

  6. Multipart form bound to a User object (including the file).

Note
File Upload

File uploads are only available for multipart requests.

Java
Kotlin
  FileUpload pic = ctx.file("pic");          // 1
  List<FileUpload> pics = 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

Session

Session parameters are available via session() or sessionOrNull(). (See the full Session Chapter for details).

Java
Kotlin
  Session session = ctx.session();                       // 1
  String attribute = ctx.session("attribute").value();   // 2
  1. Finds an existing Session or creates a new one.

  2. Gets a specific session attribute.

Flash

Flash parameters transport success/error messages between requests. They are similar to a session, but their 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. Sets a flash attribute: success.

  2. Redirects to the home page.

  3. Displays the flash attribute success (if it exists) or defaults to Welcome!.

Flash attributes are implemented using an HTTP Cookie. To customize the cookie (the default name is jooby.flash), use setFlashCookie(Cookie).

Java
Kotlin
{
  setFlashCookie(new Cookie("myflash").setHttpOnly(true));
  // or if you're fine with the default name
  getFlashCookie().setHttpOnly(true);
}
Parameter Lookup

You can search for parameters across multiple sources with an explicitly defined priority using lookup().

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;
});

If a request is made to /bar?foo=baz, the result will be foo is: baz because the query parameter takes precedence over the path parameter.

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.

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

  2. Get only the peer certificate.

Value API

The Value is a unified, type-safe API for accessing all parameter types:

  • Header

  • Path

  • Query

  • Formdata/Multipart

For learning purposes, we will demonstrate the Value features using query parameters, but keep in mind that these features apply to all parameter types.

Single value

Single values are retrieved via the 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 of methods always expects a value to exist. If the value is missing or cannot be converted to the requested type, a 400 Bad Request response is generated. Therefore, single-value parameters are implicitly required:

  1. Access parameter name as String:

    • /?name=foofoo

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

  2. Access parameter score as float:

    • /?score=11.0

    • /?score=stringBad Request(400) (Type mismatch)

    • /Bad Request(400)

  3. Access parameter enabled as boolean:

    • /?enabled=truetrue

    • /?enabled=stringBad Request(400)

    • /Bad Request(400)

  4. Access parameter decimal and convert it to a custom type (BigDecimal):

    • /?decimal=2.32.3

    • /?decimal=stringBad Request(400)

    • /Bad Request(400)

Default and Optional value

You can handle optional parameters by providing a default value or requesting an Optional object:

Java
Kotlin
{
  get("/search", ctx -> {
    String q1 = ctx.query("q").value("*:*");            // 1
    Optional<String> q2 = ctx.query("q").toOptional();  // 2
    return q1;
  });
}
  1. Retrieve variable q as a String with a fallback default value of :.

    • /search?q=foofoo

    • /search:

  2. Retrieve variable q wrapped in an Optional<String>:

    • /search?q=fooOptional[foo]

    • /searchOptional.empty

Multiple values

Multiple values for a single parameter key can be retrieved as Collections:

Java
Kotlin
{
  get("/", ctx -> {
    List<String> q = ctx.query("q").toList();                            // 1
    List<Integer> n = ctx.query("n").toList(Integer.class);              // 2
    List<BigDecimal> d = ctx.query("d").toList(BigDecimal::new);         // 3
    // ...
  });
}
  1. Parameter q as List<String>:

    • /[]

    • /?q=foo[foo]

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

  2. Parameter n as List<Integer>:

    • /[]

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

  3. Parameter d mapped to List<BigDecimal>:

    • /[]

    • /?d=1.5&d=2.0[1.5, 2.0]

Structured data

The Value API allows you to traverse and parse deeply nested structured data from a request.

Given the query: /?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. Retrieves the user node.

  2. Extracts name from the user node.

  3. Extracts pass from the user node.

  4. Safely extracts email with a fallback default.

The get(String) method takes a path and returns a nested Value node, which may or may not exist.

Syntax

The structured data decoder supports both 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 Binding

The data decoder can automatically reconstruct POJOs (Plain Old Java Objects) from:

  • URL-encoded Queries

  • application/x-www-form-urlencoded Form data

  • multipart/form-data Forms

Example Models
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 Group(String id, List<Member> members) {
    this.id = id;
    this.members = members;
  }
}

Binding a single Member: /?firstname=Pedro&lastname=Picapiedra

Java
Kotlin
{
  get("/", ctx -> {
    Member member = ctx.query(Member.class);
    // ...
  });
}

Binding a nested Member from a root node: /?member.firstname=Pedro&member.lastname=Picapiedra

Java
Kotlin
{
  get("/", ctx -> {
    Member member = ctx.query("member").to(Member.class);
    // ...
  });
}

Binding tabular/array data: /?[0]firstname=Pedro&[0]lastname=Picapiedra&[1]firstname=Pablo&[1]lastname=Marmol

Java
Kotlin
{
  get("/", ctx -> {
    List<Member> members = ctx.query().toList(Member.class);
    // ...
  });
}

Binding complex nested hierarchies: /?id=flintstones&members[0]firstname=Pedro&members[0]lastname=Picapiedra

Java
Kotlin
{
  get("/", ctx -> {
    Group group = ctx.query(Group.class);
    // ...
  });
}

POJO Binding Rules: The target POJO must follow one of these rules:

  • Have a zero-argument (default) constructor.

  • Have exactly one constructor.

  • Have multiple constructors, but only one is annotated with @Inject.

The decoder maps HTTP parameters in the following order: 1. Constructor arguments 2. Setter methods

If an HTTP parameter name is not a valid Java identifier (e.g., first-name), you must map it using the @Named annotation:

Java
Kotlin
class Member {
  public final String firstname;
  public final String lastname;

  public Member(@Named("first-name") String firstname, @Named("last-name") String lastname) {
    this.firstname = firstname;
    this.lastname = lastname;
  }
}
Value Factory

The ValueFactory allows you to register new type conversions or override existing ones globally.

Java
Kotlin
{
    var valueFactory = getValueFactory();
    valueFactory.put(MyBean.class, new MyBeanConverter());
}

import io.jooby.value.ValueConverter;

class MyBeanConverter implements ValueConverter {
  @Override
  public Object convert(Type type, Value node, ConversionHint hint) {
    // Logic to convert the 'node' into MyBean.class
    return new MyBean();
  }
}

Request Body

The raw request body is available via the 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. Reads the HTTP body as a String.

  2. Reads the HTTP body as a byte array.

  3. Reads the HTTP body as an InputStream.

Message Decoder

Request body parsing (converting the raw body into a specific object) is handled by the MessageDecoder functional interface.

public interface MessageDecoder {
  <T> T decode(Context ctx, Type type) throws Exception;
}

The MessageDecoder has a single decode method that takes the request context and the target type, returning the parsed result.

JSON Decoder 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
    return myObject;
  });
}
  1. Initialize your favorite JSON library.

  2. Register the decoder to trigger when the Content-Type header matches application/json.

  3. Read the raw body as a byte[].

  4. Parse the payload into the requested type.

  5. Inside the route, calling ctx.body(Type) automatically triggers the registered decoder.

Response Body

The response body is generated by the route handler.

Response Body Example
Java
Kotlin
{
  get("/", ctx -> {
    ctx.setResponseCode(200);                    // 1
    ctx.setResponseType(MediaType.text);         // 2
    ctx.setResponseHeader("Date", new Date());   // 3

    return "Response";                           // 4
  });
}
  1. Set the status code to 200 OK (this is the default).

  2. Set the Content-Type to text/plain (this is the default for strings).

  3. Set a custom response header.

  4. Return the response body to the client.

Message Encoder

Response encoding (converting an object into a raw HTTP response) is handled by the MessageEncoder functional interface.

public interface MessageEncoder {
  Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception;
}

The MessageEncoder has a single encode method that accepts the context and the value returned by the handler, producing an output. (Internally, Output works like a java.nio.ByteBuffer for performance reasons).

JSON Encoder 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 = new MyObject();
    return myObject;                                // 6
  });
}
  1. Initialize your favorite JSON library.

  2. Register the encoder to trigger when the client’s Accept header matches application/json.

  3. Convert the route’s result into JSON.

  4. Set the Content-Type header to application/json.

  5. Return the encoded JSON payload.

  6. The route handler returns a user-defined POJO, which is automatically intercepted and encoded.

Responses

This chapter covers special response types, including raw responses, streaming, file downloads, and non-blocking responses.

Raw

Raw responses are not processed by a message encoder. The following 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 a handler
Java
Kotlin
{
  get("/json", ctx -> {
    ctx.setContentType(MediaType.json);
    return "{\"message\": \"Hello Raw Response\"}";
  });
}

Even if a JSON encoder is installed, a raw response is always sent directly to the client bypassing the encoder.

Projections

Projections allow API consumers to request a partial representation of a resource. This feature is a more flexible, dynamic, and powerful alternative to the standard Jackson @JsonView annotation.

While inspired by the "selection set" philosophy of GraphQL, it is important to note that this is not a GraphQL implementation. Instead, it provides a "Light GraphQL" experience for RESTful endpoints, allowing you to define exactly which fields should be serialized in the JSON response without the overhead of a full GraphQL engine.

Basic Usage

To enable a projection, you wrap your response object using the Projected.wrap utility. The projection syntax is parsed and applied to the underlying object or collection.

get("/users/{id}", ctx -> {
  User user = repository.findById(ctx.path("id").value());

  return Projected.wrap(user)
      .include("(id, name, email)");
});
Comparison with @JsonView

If you have used Jackson’s @JsonView, you will find Projections far more capable:

  • Dynamic: Unlike @JsonView, which requires static class markers defined at compile-time, Projections are defined at runtime.

  • Ad-hoc: You can create any combination of fields on the fly without adding new Java interfaces or classes.

  • Deep Nesting: Projections easily handle deeply nested object graphs, whereas @JsonView can become difficult to manage with complex relationships.

Projection DSL

The include method accepts a string using a simple, nested syntax:

  • Field Selection: (id, name) returns only those two fields.

  • Nested Selection: (id, address(city, country)) selects the id and specific fields from the nested address object.

  • Wildcards: (id, address(*)) selects the id and all available fields within the address object.

  • Deep Nesting: (id, orders(id, items(name, price))) allows for recursion into the object graph.

Validation

By default, the projection engine does not validate that requested fields exist on the target class (failOnMissingProperty is false). This allows for maximum flexibility, especially when working with polymorphic types or dynamic data where certain fields may only exist on specific subclasses.

If you prefer strict enforcement to prevent API consumers from requesting non-existent fields, you can enable validation:

return Projected.wrap(data)
    .failOnMissingProperty(true)
    .include("(id, name, strictFieldOnly)");
More Information

Support for Projections extends beyond core scripting to include high-level annotations and documentation generation.

  • MVC Support: Projections can be applied to controller methods using the @Project annotation. See the MVC documentation for details.

  • OpenAPI Support: Jooby automatically generates pruned schemas for your Swagger documentation. See the OpenAPI documentation for details.

Note

Implementation Note: The Projection core API defines the structure and the DSL. The actual runtime filtering is performed by your chosen JSON module:

Streaming / Chunked

The Streaming/Chunked API is available via:

You can only call one of these methods per request. When you call one of them, Jooby automatically adds the Transfer-Encoding: chunked header if the Content-Length is missing.

All three APIs have a close method, which you must call when finished.

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 (or use in Kotlin) block so it closes automatically.

  2. Write chunks of data.

  3. Return the Context.

There is an overloaded version (primarily for Java) that lets you skip the try-with-resources block and automatically closes the writer/stream for you:

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

File Download

Use FileDownload to generate file downloads (responses with a 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 a Path as an attachment.

  2. Send a download from an InputStream inline.

Alternatively, you can use the static builder methods on FileDownload and specify the download type 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());
}

NonBlocking

From the user’s perspective, there is nothing special about writing non-blocking responses—you write your route handler the same way you usually do. However, it’s important to understand how they execute in the pipeline based on your application’s mode.

In event loop mode
Java
Kotlin
{
  mode(EVENT_LOOP);                   // 1
  use(ReactiveSupport.concurrent());  // 2

  get("/non-blocking", ctx -> {
    return CompletableFuture          // 3
        .supplyAsync(() -> {
          // ...                      // 4
        });
  });
}
  1. The application runs in the event loop.

  2. Indicates we want to go non-blocking and handle CompletableFuture responses.

  3. The value is provided from the event loop. No blocking code is permitted.

  4. The value is computed asynchronously.

Running your application in worker mode works identically, except you are allowed to make blocking calls:

In worker mode
Java
Kotlin
{
  mode(WORKER);                       // 1
  use(ReactiveSupport.concurrent());  // 2

  get("/blocking", ctx -> {
    return CompletableFuture          // 3
        .supplyAsync(() -> {
          // ...                      // 4
        });
  });
}
  1. The application runs in worker mode.

  2. Indicates we want to go non-blocking.

  3. The value is provided from the worker thread. Blocking code is permitted.

  4. The value is computed asynchronously.

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

In default mode
Java
Kotlin
{
  mode(DEFAULT);                      // 1
  use(ReactiveSupport.concurrent());  // 2

  get("/non-blocking", ctx -> {
    return CompletableFuture          // 3
        .supplyAsync(() -> {
          // ...                      // 4
        });
  });
}
Note

For all reactive frameworks below, explicit handler setup (e.g., use(Reactivex.rx())) is only required for Script/Lambda routes. For MVC routes, Jooby automatically configures the handler based on the route’s return type.

CompletableFuture

CompletableFuture is a non-blocking type that produces a single result:

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

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

1) Add the SmallRye Mutiny dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-mutiny</artifactId>
  <version>4.0.16</version>
</dependency>
Uni (Single Value)
Java
Kotlin
import io.jooby.mutiny;
import io.smallrye.mutiny.Uni;

{
  use(Mutiny.mutiny());

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

{
  use(Mutiny.mutiny());

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

For Multi, Jooby automatically builds a chunked response. Each item in the stream is sent to the client as a new HTTP chunk.

RxJava

1) Add the RxJava dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-rxjava3</artifactId>
  <version>4.0.16</version>
</dependency>
Single
Java
Kotlin
import io.jooby.rxjava3.Reactivex;

{
  use(Reactivex.rx());

  get("/non-blocking", ctx -> {
    return Single
        .fromCallable(() -> "Single")
        .map(it -> "Hello " + it);
  });
}
Flowable (Chunked Stream)
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, sending each item as a separate chunk.

Reactor

1) Add the Reactor dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-reactor</artifactId>
  <version>4.0.16</version>
</dependency>
Mono (Single Value)
Java
Kotlin
import io.jooby.Reactor;

{
  use(Reactor.reactor());

  get("/non-blocking", ctx -> {
    return Mono
        .fromCallable(() -> "Mono")
        .map(it -> "Hello " + it);
  });
}
Flux (Chunked Stream)
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, sending each item as a separate chunk.

Kotlin Coroutines

(Note: Coroutines are exclusive to Kotlin, so there is no Java equivalent for this section).

Coroutine handler:
{
  coroutine {
    get("/") {
      delay(100)            // 1
      "Hello Coroutines!"   // 2
    }
  }
}
  1. Call a suspending function.

  2. Send the response to the client.

Using an extension and suspending function:
{
  coroutine {
    get("/") {
      ctx.doSomething()          // 1
    }
  }
}

suspend fun Context.doSomething(): String {
  delay(100)                   // 2
  return "Hello Coroutines!"   // 3
}
  1. Call an extension suspending function.

  2. Safely perform a suspending or blocking call.

  3. Send the response to the client.

Coroutines work like any other non-blocking type. If you start Jooby using the event loop or default mode, Jooby creates a coroutine context to execute it.

Jooby uses the worker executor to create a coroutine context. This executor is provided by the web server implementation, unless you provide a custom one:

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

  coroutine {
    get("/") {
      val n = 5 * 5         // 1
      delay(100)            // 2
      "Hello Coroutines!"   // 3
    }
  }
}
  1. Statement runs in the worker executor (cached thread pool).

  2. Calls a suspending function.

  3. Produces a response.

By default, Coroutines always run in the worker executor. However, Jooby provides an experimental API where coroutines run in the caller thread (the event loop) until a suspending function is found. You can enable this by setting the coroutineStart option:

UNDISPATCHED Start
{
  coroutine(CoroutineStart.UNDISPATCHED) {
    get("/") {
      val n = 5 * 5         // 1
      delay(100)            // 2
      "Hello Coroutines!"   // 3
    }
  }
}
  1. Statement runs in the event loop (caller thread).

  2. Calls a suspending function and dispatches to the worker executor.

  3. Produces a response from the worker executor.

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

Customizing the Context
{
  coroutine {
    launchContext { MDCContext() }  // 1

    get("/") {
      // ...
    }
  }
}
  1. The launchContext lambda runs before launching each coroutine. It allows you to customize the CoroutineContext for the request (e.g., to store/restore MDC, transactions, or other request-scoped data).

Send Methods

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

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

Although these methods operate via side-effects, the route handler must still return a value. All send methods return the current Context. Returning the context signals to Jooby that the response was already handled and the standard route output should be ignored.

The family of send methods includes:

Built-in Handlers

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

AccessLogHandler

The AccessLogHandler logs all incoming requests using the NCSA format (also known as the Common Log Format).

Usage
Java
Kotlin
import io.jooby.handler.AccessLogHandler;

{
  use(new AccessLogHandler());  // 1

  get("/", ctx -> "Logging...");
}
  1. Install the AccessLogHandler as a global decorator.

Once installed, it prints a message to your log similar to this:

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

The message components are:

  • Remote Address: The IP address of the client.

  • User ID: Usually a dash (-) unless specified.

  • Date and Time: The timestamp of the request.

  • Request Line: The HTTP method, request path, and protocol.

  • Status Code: The HTTP response status code.

  • Content-Length: The size of the response in bytes (or - if missing).

  • Latency: The total time taken to process the request in milliseconds.

Custom Headers

You can append specific request or response headers to the log entry using the following methods:

Tip

If your application runs behind a reverse proxy (like Nginx or AWS ALB) that sends X-Forwarded-* headers, remember to enable the trust proxy option to ensure the correct remote IP address is logged.

CorsHandler

Cross-Origin Resource Sharing (CORS) is a mechanism that uses HTTP headers to grant a web application running at one origin permission to access resources from a server at a different origin.

By default, Jooby rejects all cross-origin requests. To enable them, you must install the CorsHandler.

CORS Usage
Java
Kotlin
import io.jooby.handler.CorsHandler;

{
  use(new CorsHandler());  // 1

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

The default configuration is:

  • Origin: * (All origins)

  • Credentials: true

  • Methods: GET, POST

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

  • Max Age: 30m

Customizing CORS Options

To customize these settings, use the Cors class:

CORS Options
Java
Kotlin
import io.jooby.handler.Cors;
import io.jooby.handler.CorsHandler;

{
  Cors cors = new Cors()
     .setMethods("GET", "POST", "PUT", "DELETE");  // 1

  use(new CorsHandler(cors));                      // 2
}
  1. Specify the allowed HTTP methods.

  2. Pass the custom configuration to the CorsHandler.

Configuration-based CORS

You can also define your CORS settings directly in your application.conf file:

application.conf
cors {
  origin: "https://example.com"
  credentials: true
  methods: [GET, POST, PUT]
  headers: [Content-Type, X-App-Version]
  maxAge: 30m
  exposedHeaders: [X-Custom-Header]
}

Then, load the configuration into the handler:

Loading from Config
Java
Kotlin
import io.jooby.handler.Cors;
import io.jooby.handler.CorsHandler;

{
  Cors cors = Cors.from(getConfig());  // 1
  use(new CorsHandler(cors));
}
  1. Loads the CORS options defined in the cors namespace of your configuration file.

CsrfHandler

The Cross-Site Request Forgery (CSRF) Handler protects your application against unauthorized commands performed on behalf of an authenticated user.

Jooby generates a unique CSRF token for each active user session. 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 that performs a state-changing operation (like POST), you must include the CSRF token.

HTML Form with CSRF
<form method="POST" action="/update-profile">
    <input name="csrf" value="{{csrf}}" type="hidden" />
    <button type="submit">Update</button>
</form>

In the example above, {{csrf}} is a request attribute created by the handler. When the form is submitted, the CsrfHandler automatically verifies that the token in the request matches the token stored in the user’s session.

Usage

CSRF protection requires an active Session Store to be configured first.

CSRF Usage
Java
Kotlin
import io.jooby.handler.CsrfHandler;

{
  // 1. Session store is required
  setSessionStore(SessionStore.memory());

  // 2. Install CSRF handler
  use(new CsrfHandler());

  get("/form", ctx -> {
    // Token is available as a request attribute
    return new ModelAndView("form.hbs", Map.of("csrf", ctx.getAttribute("csrf")));
  });
}
Token Delivery

By default, the handler looks for a token named csrf in the following order:

  1. HTTP Header: X-CSRF-Token or csrf

  2. Cookie: csrf

  3. Form Parameter: csrf

Customization

You can customize the behavior of the handler using the following methods:

Tip

If you are building a Single Page Application (SPA), you can configure the handler to read the token from a custom header and have your frontend (e.g., React or Vue) send it back on every request.

GracefulShutdownHandler

The GracefulShutdown extension allows the application to finish processing active requests before the JVM exits.

Once a shutdown is initiated, the extension interceptor ensures that all new incoming requests are rejected with a Service Unavailable (503) status code, while existing requests are allowed to complete within a specified grace period.

Usage
Java
Kotlin
import io.jooby.GracefulShutdown;

{
  install(new GracefulShutdown());  // 1

  get("/", ctx -> "Hello!");
}
  1. Install the GracefulShutdown extension.

Timeout

By default, the extension waits for all requests to finish. You can optionally specify a maximum duration to wait before forcing the application to stop:

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

This extension must be installed at the very beginning of your route pipeline to ensure it can intercept and manage all incoming traffic during the shutdown phase.

HeadHandler

By default, Jooby does not automatically handle HTTP HEAD requests. To support them without manually defining head(…​) routes for every resource, you can use the HeadHandler.

The HeadHandler automatically routes HEAD requests to your existing GET handlers.

Usage
Java
Kotlin
import io.jooby.handler.HeadHandler;

{
  use(new HeadHandler());  // 1

  get("/", ctx -> "Full response body");
}
  1. Install the HeadHandler.

When a HEAD request is received, the corresponding GET handler is executed to compute response metadata (like Content-Length and other headers), but the actual response body is stripped before being sent to the client.

Note

This handler is a convenient way to support HEAD requests globally, but keep in mind that the GET handler logic still runs. If your GET handler performs expensive operations to generate a body, you might prefer a custom implementation that skips body generation for HEAD requests.

RateLimitHandler

The RateLimitHandler provides request throttling using the popular Bucket4j library.

To use this handler, add the following dependency to your project:

Maven
Gradle
<dependency>
  <groupId>com.bucket4j</groupId>
  <artifactId>bucket4j_jdk17-core</artifactId>
  <version>8.16.1</version>
</dependency>
Basic Usage

The simplest configuration applies a global limit to all incoming requests.

10 requests per minute (Global)
Java
Kotlin
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.jooby.handler.RateLimitHandler;

{
  Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
  Bucket bucket = Bucket4j.builder().addLimit(limit).build();      // 1

  before(new RateLimitHandler(bucket));                            // 2
}
  1. Create a bucket with the desired capacity and refill rate.

  2. Install the RateLimitHandler as a before filter.

Throttling by Key

Often, you want to limit requests per user, API key, or IP address rather than globally.

Throttling by Remote IP
Java
Kotlin
{
  before(new RateLimitHandler(remoteAddress -> {
    Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
    return Bucket4j.builder().addLimit(limit).build();
  }));
}
Throttling by API Key Header
Java
Kotlin
{
  before(new RateLimitHandler(key -> {
    Bandwidth limit = Bandwidth.simple(50, Duration.ofHours(1));
    return Bucket4j.builder().addLimit(limit).build();
  }, "X-API-Key"));
}
Clustered Rate Limiting

If you are running multiple Jooby instances, you can use a distributed bucket using Bucket4j’s ProxyManager. This allows the rate limit state to be shared across the cluster via a backend like Redis or Hazelcast.

Cluster Support
Java
Kotlin
{
  ProxyManager<String> buckets = ...; // Configure your backend (Redis, etc.)

  before(RateLimitHandler.cluster(key -> {
    return buckets.getProxy(key, () -> {
      return Bucket4j.configurationBuilder()
          .addLimit(Bandwidth.simple(100, Duration.ofMinutes(1)))
          .build();
    });
  }));
}

For more details on setting up backends like Redis, Hazelcast, or Infinispan, refer to the Bucket4j documentation.

SSLHandler

The SSLHandler forces clients to use HTTPS by automatically redirecting all non-secure (HTTP) requests to their secure (HTTPS) version.

Before using this handler, ensure you have followed the instructions in the HTTPS Support section to enable SSL on your server.

Usage
Java
Kotlin
import io.jooby.handler.SSLHandler;

{
  before(new SSLHandler());  // 1

  get("/", ctx -> "You are on: " + ctx.getScheme());
}
  1. Install the SSLHandler as a before filter.

The handler reconstructs the HTTPS URL using the Host header. If your application is running behind a load balancer or reverse proxy, you must enable the trust proxy option to ensure the handler correctly identifies the client’s original protocol and host.

Customizing the Host

By default, the handler redirects to the host provided in the request. You can explicitly specify a destination host if needed:

Explicit Host Redirect
Java
Kotlin
{
  // Redirects all traffic to https://myhost.org
  before(new SSLHandler("myhost.org"));
}
Tip

For production environments behind a reverse proxy (like Nginx, HAProxy, or an AWS ALB) that terminates SSL, the SSLHandler relies on the X-Forwarded-Proto header. Make sure your proxy is configured to send it and that you have enabled setTrustProxy(true) in your Jooby configuration.

TraceHandler

By default, Jooby does not support HTTP TRACE requests. To enable them for debugging or diagnostic purposes, you can use the TraceHandler.

The TraceHandler implements a loop-back test by returning the received request message back to the client as the entity body of a 200 (OK) response.

Usage
Java
Kotlin
import io.jooby.handler.TraceHandler;

{
  use(new TraceHandler());  // 1

  get("/", ctx -> "Hello World");
}
  1. Install the TraceHandler as a decorator.

When a TRACE request is received, the handler bypasses the normal route logic and echoes the request headers and body back to the client. This is useful for seeing what transformations are being applied to the request by intermediate proxies or firewalls.

Warning

HTTP TRACE can potentially expose sensitive information (like cookies or authentication headers) to malicious scripts (Cross-Site Tracing). It is generally recommended to enable this handler only in development or strictly controlled diagnostic environments.

Error Handler

Jooby catches application exceptions using the ErrorHandler class. By default, the DefaultErrorHandler produces a simple HTML page or a JSON response based on the request’s Accept header, and logs the exception.

HTML output:
Not Found
message: Page not found
status code: 404
JSON output:
{
  "message": "Page not found",
  "status": 404,
  "reason": "Not Found"
}
Log output:
GET /xx 404 Not Found
io.jooby.exception.StatusCodeException: Not found
  at ...

The StatusCodeException is a generic exception that lets you explicitly specify an HTTP status code:

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

Several standard Java exceptions are automatically mapped to default status codes:

  • IllegalArgumentException (and subclasses): 400 BAD_REQUEST

  • NoSuchElementException (and subclasses): 400 BAD_REQUEST

  • FileNotFoundException (and subclasses): 404 NOT_FOUND

  • Exception (and all other subclasses): 500 SERVER_ERROR

To map a custom exception to a specific status code, register it using the errorCode method:

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

Custom Error Handler

You can override the default behavior and provide a custom global error handler using the error(ErrorHandler) method:

Global 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("Encountered a " + statusCode.value() + " error");             // 3
  });
}
  1. Register a global (catch-all) exception handler.

  2. Log the error.

  3. Send a custom error response to the client.

You can use the render(Object) method inside the error handler to delegate the response to a registered MessageEncoder or TemplateEngine.

The next example produces an HTML or JSON response based on the client’s Accept header using content negotiation:

Error Handling with Content Negotiation
Java
Kotlin
import static io.jooby.MediaType.json;

{
  install(new MyTemplateEngineModule());                       // 1
  install(new MyJsonModule());                                 // 2

  error((ctx, cause, statusCode) -> {
    ctx.getRouter().getLog().error("Error: {}", statusCode.value(), cause);

    Map<String, Object> errorData = Map.of("message", cause.getMessage());

    if (ctx.accept(json)) {                                    // 3
      ctx.render(errorData);                                   // 4
    } else {
      ctx.render(new ModelAndView("error.template", errorData));  // 5
    }
  });
}
  1. Install a template engine module.

  2. Install a JSON module.

  3. Check if the Accept header prefers application/json.

  4. Render the JSON response if matched.

  5. Fallback to rendering an HTML template.

Catch by Code

In addition to the global error handler, you can register handlers for specific HTTP status codes:

Status Code Handler
Java
Kotlin
import static io.jooby.StatusCode.NOT_FOUND;

{
  error(NOT_FOUND, (ctx, cause, statusCode) -> {
    ctx.send(statusCode);    // 1
  });
}
  1. Send a silent 404 response to the client.

In this example, we silence all 404 responses by bypassing the logging system and sending an empty response body.

Tip

The send(StatusCode) method sends an empty HTTP response with the specified status code.

Catch by Exception

You can also register handlers for specific exception types. This is useful for intercepting business-logic exceptions before they hit the global handler:

Exception Type Handler
Java
Kotlin
{
  error(MyBusinessException.class, (ctx, cause, statusCode) -> {
    // Log and handle MyBusinessException specifically
  });
}

Problem Details

Most APIs need a structured way to report errors, helping users understand exactly what went wrong. While you could invent a custom error-reporting format, it requires effort to design and forces your clients to learn a non-standard schema.

Instead, you can adopt the standard defined in IETF RFC 7807 (later refined by RFC 9457). Adopting this standard saves you time and benefits your users by providing a familiar, widely supported error format.

Jooby provides built-in, native support for RFC 7807 Problem Details.

Setup

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

application.conf
problem.details.enabled = true

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

You can customize the behavior using these additional properties:

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) are logged. You can enable logging for client errors (4xx) as well. (If your logger is set to DEBUG, the log will also include the stack trace).

  2. Mute logging entirely for specific HTTP status codes.

  3. Mute logging entirely for specific Exception classes.

Creating Problems

The HttpProblem class represents the RFC 7807 model. Because it extends RuntimeException, you can throw it naturally just like any other exception.

Static Helpers

There are several static methods to quickly produce an HttpProblem:

There are also shorthands for common HTTP errors:

  • HttpProblem.badRequest(…​) (400)

  • HttpProblem.notFound(…​) (404)

  • HttpProblem.unprocessableEntity(…​) (422)

  • HttpProblem.internalServerError() (500)

Throwing an HttpProblem
Java
Kotlin
import io.jooby.problem.HttpProblem;
import io.jooby.StatusCode;

{
  get("/users/{userId}", ctx -> {
    String 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 " + userId + " was not found in the system."
      );
    }
    // ...
  });
}

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
}
Builder

For complex errors, use the builder to construct a rich problem instance with all standard properties:

Using the Builder
Java
Kotlin
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();
Extra Parameters

RFC 7807 allows you to add custom properties to the problem details object. To make serialization easier (especially in statically typed languages), Jooby groups all extra properties under a single root field called parameters.

You can add parameters via the builder:

Adding Parameters
Java
Kotlin
throw HttpProblem.builder()
  .title("Order not found")
  .status(StatusCode.NOT_FOUND)
  .detail("Order with ID " + orderId + " could not be processed.")
  .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:

{
  "title": "Order not found",
  "status": 404,
  "detail": "Order with ID 123 could not be processed.",
  "parameters": {
    "reason": "Order ID format incorrect or order does not exist.",
    "suggestion": "Please check the order ID and try again.",
    "supportReference": "/support"
  }
}
Custom Headers

Some HTTP responses (like 413 Payload Too Large or 426 Upgrade Required) require specific response headers. You can append headers directly to your HttpProblem:

Adding Headers
Java
Kotlin
throw HttpProblem.builder()
  .title("Invalid input parameters")
  .status(StatusCode.UNPROCESSABLE_ENTITY)
  .header("my-string-header", "string-value")
  .header("my-int-header", 100)
  .build();
Error Details (RFC 9457)

RFC 9457 introduced a standard way to deliver bulk validation errors via an errors array. You can add these using the error() or errors() methods in the builder:

Adding Validation Errors
Java
Kotlin
throw HttpProblem.builder()
  .title("Validation Failed")
  .status(StatusCode.BAD_REQUEST)
  .error(new HttpProblem.Error("First name cannot be blank", "/firstName"))
  .error(new HttpProblem.Error("Last name is required", "/lastName"))
  .build();

Resulting Response:

{
  "title": "Validation Failed",
  "status": 400,
  "errors": [
    {
      "detail": "First name cannot be blank",
      "pointer": "/firstName"
    },
    {
      "detail": "Last name is required",
      "pointer": "/lastName"
    }
  ]
}
Tip

If you need to enrich validation errors with more information, you can extend HttpProblem.Error to create your own custom error model.

Mapping Custom Exceptions

If your application already uses a suite of custom exception classes, you don’t need to rewrite them. Make them Problem Details-compliant by implementing the HttpProblemMappable interface:

Mapping Existing Exceptions
Java
Kotlin
import io.jooby.problem.HttpProblemMappable;

public class MyBusinessException extends RuntimeException implements HttpProblemMappable {

  @Override
  public HttpProblem toHttpProblem() {
    return HttpProblem.builder()
      .title("Business Logic Violation")
      .status(StatusCode.CONFLICT)
      .detail(this.getMessage())
      .build();
  }
}
Custom Problem Types

You can easily define domain-specific problem types by extending HttpProblem and utilizing the builder:

Custom Problem Class
Java
Kotlin
public class OutOfStockProblem extends HttpProblem {

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

  public OutOfStockProblem(String product) {
    super(builder()
      .type(TYPE)
      .title("Out of Stock")
      .status(StatusCode.BAD_REQUEST)
      .detail("The product '" + product + "' is no longer available.")
      .param("suggestions", List.of("Grinder MX-17", "Grinder MX-25"))
      .build()
    );
  }
}
Custom Exception Handlers

The features above allow you to rely entirely on Jooby’s built-in global error handler. However, if you have a niche use case that requires a custom exception handler, you can still catch the exception and manually delegate it to the Problem Details renderer:

Custom Handler Delegation
Java
Kotlin
{
  error(MyCustomException.class, (ctx, cause, code) -> {
    MyCustomException ex = (MyCustomException) cause;

    HttpProblem problem = HttpProblem.valueOf(code, ex.getMessage());   // 1

    ctx.getRouter().getErrorHandler().apply(ctx, problem, code);        // 2
  });
}
  1. Transform the custom exception into an HttpProblem.

  2. Propagate the problem back to the global ProblemDetailsHandler to ensure standard rendering.

Important

Do not attempt to render HttpProblem manually. Because HttpProblem derives from RuntimeException, it contains a stack trace. If you render it manually without passing it to the global error handler, you risk accidentally exposing the raw stack trace to the client. Always propagate the problem back to the router’s error handler so it can be safely sanitized and formatted.

Execution Model

Jooby is a flexible, performant micro-framework that provides both blocking and non-blocking APIs for building web applications in Java and Kotlin.

In this chapter, we will cover the Jooby execution model, specifically:

  • Executing code on the event loop.

  • Safely executing blocking code.

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

Mode

Event Loop

The EVENT_LOOP mode allows you to run route handlers directly on the event loop (a.k.a. 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 an advanced execution model that requires careful application design, because BLOCKING IS STRICTLY FORBIDDEN on the event loop thread.

What if you need to block?

The dispatch(Runnable) operator shifts execution to a worker executor, which safely allows blocking calls (like database queries or remote service calls):

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!");

    dispatch(() -> {
      // All routes defined inside this block are allowed to block:
      get("/db-list", ctx -> {
        // Safe to block!
        Object result = fetchFromDatabase();
        return result;
      });
    });
  }

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

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

However, you can provide your own custom worker executor at the application level or specifically for a single dispatch block:

Java
Kotlin
import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;
import java.util.concurrent.Executors;

public class App extends Jooby {
  {
    // Application-level executor
    worker(Executors.newCachedThreadPool());

    // Dispatches to the application-level executor (the cached thread pool)
    dispatch(() -> {
      // ...
    });

    // Dispatches to an explicit, custom executor
    var cpuIntensive = Executors.newSingleThreadExecutor();
    dispatch(cpuIntensive, () -> {
      // ...
    });
  }

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

The WORKER mode allows you to make blocking calls from any route handler (a.k.a. blocking mode). You can write code sequentially without worrying about blocking the server.

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 = fetchFromDatabase();
      return result;
    });
  }

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

Just like in EVENT_LOOP mode, you can override the default server worker and provide your own custom executor:

Java
Kotlin
import static io.jooby.ExecutionMode.WORKER;
import static io.jooby.Jooby.runApp;
import java.util.concurrent.Executors;

public class App extends Jooby {
  {
    worker(Executors.newCachedThreadPool());

    get("/", ctx -> {
      // Safe to block! Handled by the cached thread pool.
      Object result = fetchFromDatabase();
      return result;
    });
  }

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

When running in WORKER mode, Jooby implicitly applies a dispatch call to the worker executor for every route.

Default

The DEFAULT execution mode is a smart hybrid between the WORKER and EVENT_LOOP modes. As the name implies, this is the default execution mode in Jooby.

Jooby analyzes the return type of your route handler to determine which execution mode fits best. If the response type is non-blocking, it executes on the event loop. Otherwise, it dispatches to the worker executor.

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

  • A CompletableFuture

  • An RxJava type (e.g., Single, Flowable)

  • A Reactor type (e.g., Mono, Flux)

  • A Kotlin Coroutine

Java
Kotlin
import static io.jooby.Jooby.runApp;
import java.util.concurrent.CompletableFuture;

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; this route executes directly on the event loop.

  2. String is a blocking type; this route is dispatched to the worker executor.

Tip

You are free to return non-blocking types while running in explicit WORKER or EVENT_LOOP modes. Non-blocking response types are not exclusive to DEFAULT mode; all DEFAULT mode does is dynamically decide whether to dispatch to a worker based on that return type.

Worker Executor

This section details the default worker executors provided by the underlying web servers. The worker executor is used when:

  • The application mode is set to WORKER (or DEFAULT returning a blocking type).

  • The application mode is set to EVENT_LOOP and an explicit dispatch block is used.

Each web server provides its own default worker executor tuning:

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

workerThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2) * 8
  • Undertow: The Undertow server implementation multiplies the number of available processors by 8.

workerThreads = Runtime.getRuntime().availableProcessors() * 8
  • Jetty: The Jetty server implementation uses a default configuration of 200 worker threads.

These are sensible defaults provided by the server implementations. If you need to increase or decrease the number of worker threads globally, you can configure the server:

Java
Kotlin
{
  configureServer(server -> {
    server.setWorkerThreads(100);
  });
}

Web

Everything you need to handle HTTP traffic and build robust APIs or web applications. Explore Jooby’s expressive routing paradigms, request and response handling, content negotiation, and advanced web features like WebSockets and file uploads.

MVC API

The MVC API provides an annotation-driven alternative to the Script API for defining routes. Jooby uses an annotation processor to generate source code that defines and executes these routes. By default, the generated classes are suffixed with an underscore (_).

If you use Gradle 6.0 or later, or a modern Maven setup, Jooby leverages incremental annotation processing. This means the compiler only processes classes that have changed since the last build, significantly speeding up compilation times.

You can control incremental processing via compiler arguments:

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

By setting jooby.incremental=false, you disable incremental processing entirely, forcing a full recompilation of the project every time. (Defaults to true).

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

MVC API Example:
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 base path pattern. The @Path annotation can be applied at the class or method level.

  2. Define the HTTP method.

  3. Register the generated controller (Controller_) in the main application.

Getting Started

To quickly create a new MVC project, use the jooby console:

jooby create myapp --mvc

The Jooby CLI automatically configures the Maven/Gradle build and sets up the annotation processor for you.

Registration

Unlike some frameworks, Jooby does not use classpath scanning. MVC routes must be explicitly registered in your application configuration:

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) method installs the MVC routes. You can pass an instance directly, or if the controller constructor is annotated with @Inject (e.g., jakarta.inject.Inject), the generated code will attempt to resolve the dependencies from the registry.

Parameters

HTTP parameter extraction is handled via @*Param annotations.

You can also use the generic Param annotation to extract a parameter from multiple sources with a specific fallback order.

Header

Extract headers using the HeaderParam annotation:

Headers
Java
Kotlin
public class MyController {

  @GET
  public String handle(@HeaderParam String token) {   // 1
    // ...
  }
}
  1. Accesses the HTTP header named token.

Unlike JAX-RS, specifying the parameter name inside the annotation is optional. Jooby infers it from the variable name. However, you must provide the name explicitly if the HTTP header is not a valid Java identifier:

Invalid Java Identifier
Java
Kotlin
public class MyController {

  @GET
  public String handle(@HeaderParam("Last-Modified-Since") long lastModifiedSince) {
    // ...
  }
}

Extract cookies using the CookieParam annotation:

Cookies
Java
Kotlin
public class MyController {

  @GET
  public String handle(@CookieParam String token) {   // 1
    // ...
  }
}
  1. Accesses the cookie named token.

As with headers, provide the explicit name if the cookie key contains dashes or invalid Java characters (e.g., @CookieParam("token-id") String tokenId).

Path

Extract path variables using the PathParam annotation:

PathParam
Java
Kotlin
public class MyController {

  @Path("/{id}")
  public String handle(@PathParam String id) {
    // ...
  }
}
Query

Extract query string variables using the QueryParam annotation:

QueryParam
Java
Kotlin
public class MyController {

  @Path("/")
  public String handle(@QueryParam String q) {
    // ...
  }
}
Formdata / Multipart

Extract form-data or multipart parameters using the FormParam annotation:

FormParam
Java
Kotlin
public class MyController {

  @POST("/")
  public String handle(@FormParam String username) {
    // ...
  }
}
Request Body

The HTTP request body does not require an explicit annotation. Simply define the POJO in the method signature:

HTTP Body
Java
Kotlin
public class MyController {

  @POST("/")
  public String handle(MyObject body) {
    // ...
  }
}
Bind

The BindParam annotation allows for custom data binding from the HTTP request directly into an object.

Using the annotation
Java
Kotlin
public class Controller {

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

  public static MyBean of(Context ctx) {
    // Build MyBean entirely from the Context
    return new MyBean(ctx.path("foo").value());
  }
}

How @BindParam works:

  • It looks for a public method/function on the target class that accepts a Context and returns the target type.

  • By default, it looks for this factory method on the parameter type itself (MyBean), but will fall back to searching the Controller class.

Alternatively, you can specify a distinct factory class and/or method name:

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

Extract flash attributes using the FlashParam annotation:

Flash
Java
Kotlin
public class MyController {

  @GET
  public String handle(@FlashParam String success) {   // 1
    // ...
  }
}
  1. Accesses the flash attribute named success.

Session

Extract specific session attributes using the SessionParam annotation:

Session Attribute
Java
Kotlin
public class MyController {

  @GET
  public String handle(@SessionParam String userId) {   // 1
    // ...
  }
}
  1. Accesses the session attribute named userId.

You can also request the entire Session object:

Session Object
Java
Kotlin
public class MyController {

  @GET
  public String handle(Session session) {   // 1
    // ...
  }
}
  1. If no session exists yet, a new session will be created. To avoid this and only retrieve an existing session, use Optional<Session> as the parameter type.

Context

Extract specific context attributes using the ContextParam annotation:

Context Attribute
Java
Kotlin
public class MyController {

  @GET
  public String handle(@ContextParam String userId) {   // 1
    // ...
  }
}
  1. Accesses the context attribute named userId.

You can also request all attributes at once:

All Context Attributes
Java
Kotlin
public class MyController {

  @GET
  public String handle(@ContextParam Map<String, Object> attributes) {   // 1
    // ...
  }
}
  1. To retrieve all context attributes, the parameter must be typed as a Map<String, Object> (or Map<String, Any> in Kotlin).

Multiple Sources

Use the Param annotation to search for a parameter across multiple sources with an explicitly defined fallback order:

Multiple Sources
Java
Kotlin
import static io.jooby.annotation.ParamSource.QUERY;
import static io.jooby.annotation.ParamSource.PATH;

public class FooController {

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

If a request is made to /bar?foo=baz, the result will be foo is: baz because the QUERY parameter takes precedence over the PATH parameter in the annotation array.

Responses

Projections

The MVC module provides first-class support for Projections via annotations. This allows you to define the response view declaratively, keeping your controller logic clean and focused on data retrieval.

Usage

There are two ways to define a projection in an MVC controller.

You can annotate your method with @Project and provide the selection DSL:

Via @Project Annotation
@GET
@Project("(id, name)")
public List<User> listUsers() {
  return service.findUsers();
}

Alternatively, you can define the projection directly within the HTTP method annotation (e.g., @GET, @POST) using the projection attribute:

Via HTTP Method Attribute
@GET(value = "/users", projection = "(id, name, email)")
public List<User> listUsers() {
  return service.findUsers();
}
Automatic Wrapping

The Jooby Annotation Processor automatically handles the conversion of your method’s return type. You are not forced to return a Projected instance; you can simply return your POJO or Collection, and Jooby will wrap it for you at compile-time.

However, if you need manual control (for example, to dynamically toggle validation), you can still return a Projected instance explicitly:

@GET
public Projected<User> getUser(String id) {
  User user = service.findById(id);
  return Projected.wrap(user)
      .failOnMissingProperty(true)
      .include("(id, status)");
}
Note

For more details on the Selection DSL syntax and available JSON engines, please refer to the Core Projections documentation.

Status Code

The default HTTP status code returned by an MVC route is 200 OK, except for void methods annotated with @DELETE, which automatically return 204 No Content.

If you need to return a different status code, you have two options: 1. Inject the Context into your method and call setResponseCode(StatusCode). 2. Return a StatusCode instance directly from the method.

NonBlocking

Any MVC method returning a non-blocking type (CompletableFuture, Single, Maybe, Flowable, Mono, Flux) is automatically handled as a non-blocking route.

Kotlin suspend functions are also supported natively:

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) {
    mvc(SuspendMvc_())
  }
}

A non-blocking route runs on the event loop by default, where blocking is NOT allowed. For more details, see the NonBlocking Responses section.

Execution Model

MVC routes follow the standard Jooby Execution Model.

By default, if your route returns a blocking type (like a String or a POJO), Jooby automatically dispatches the execution to the worker executor. If it returns a non-blocking type (or is a suspend function), it runs on the event loop.

If you need explicit control over where a specific blocking MVC route executes, use the Dispatch annotation:

Dispatch Annotation
Java
Kotlin
public class MyController {

  @GET("/blocking")
  @Dispatch               // 1
  public String blocking() {
    return "I'm blocking";
  }
}
  1. Forces the route to run in the WORKER executor, safely allowing blocking calls.

The Dispatch annotation also supports routing execution to a named, custom executor:

Dispatch to custom executor
Java
Kotlin
public class MyController {

  @GET("/blocking")
  @Dispatch("single")          // 1
  public String blocking() {
    return "I'm blocking";
  }
}
  1. Dispatches execution to the executor registered under the name single.

The custom executor must be registered in the application before the MVC route is registered:

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

  mvc(new MyController_());
}

JAX-RS Annotations

Alternatively, 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!";
  }
}

These annotations work exactly like the Jooby native MVC annotations.

(Note: Jooby does not implement the full JAX-RS specification, nor is there a plan to do so. Support for these annotations exists primarily to allow integration with third-party tools, like Swagger/OpenAPI generators, that rely on them).

Generated Router

For each MVC controller class, a new class is generated ending with an underscore (_). This generated class mimics the constructors of the source class. (If the constructor is annotated with @Inject, a default constructor is automatically generated).

Any annotations found on the controller methods will be persisted as route attributes, unless explicitly excluded by the jooby.skipAttributeAnnotations compiler option.

You can access the generated routes at runtime:

{
  var routes = mvc(new MyController_());
  routes.forEach(route -> {
    // Modify or inspect the route
  });
}

Annotation Processor Options

Option Type Default Description

jooby.debug

boolean

true

Runs the annotation processor in debug mode.

jooby.incremental

boolean

true

Hints to Maven/Gradle to perform incremental compilation. Essential for fast development iteration.

jooby.skipAttributeAnnotations

array

[]

A comma-separated list of annotations to skip during bytecode generation (i.e., do not attach them as route attributes).

jooby.mvcMethod

boolean

false

Sets the Route.mvcMethod property on the generated route when true.

jooby.routerPrefix

string

Adds a prefix to the generated class name.

jooby.routerSuffix

string

_

Sets the suffix for the generated class name.

Setting Options
Maven
Gradle
  <plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
      <annotationProcessorPaths>
        <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.skipAttributeAnnotations=FooAnnotation,BarAnnotation</compilerArg>
      </compilerArgs>
    </configuration>
  </plugin>
Important

The execution order of annotation processors is critical. If you are using Lombok and Avaje Inject alongside Jooby, the configuration order must be: lombokavaje-injectjooby-apt.

Templates

Templates are rendered using a ModelAndView object and require a TemplateEngine implementation to be installed.

Basic Rendering
Java
Kotlin
{
  install(new MyTemplateEngineModule());            // 1

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

  2. Build the view model.

  3. Return a ModelAndView instance containing the template path and the model data.

You can explicitly set the desired locale for template rendering on the ModelAndView object:

Setting the Locale
Java
Kotlin
{
  install(new MyTemplateEngineModule());

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

If no locale is specified explicitly, Jooby falls back to the locale matched by the Accept-Language header of the current request.

Note

Not all template engines support localized rendering. If you use a template engine that doesn’t support it, setting the locale will have no effect.

Template Engine

A template engine handles the actual view rendering. A template engine extends the MessageEncoder interface, accepting a ModelAndView instance and producing a String result.

The extensions() method lists the file extensions that the template engine supports. The default file extension is .html.

Jooby uses the file extension of the requested template to locate the correct template engine. If a template engine for the specified file extension isn’t found, an IllegalArgumentException is thrown.

This file-extension routing allows you to easily use multiple template engines side-by-side in the same application:

Multiple Template Engines
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. Renders using Handlebars due to the .hbs extension.

  4. Renders using Freemarker due to the .ftl extension.

Check out the Modules section for a full list of supported template engines.

View Model

Since Jooby 3.1.x, the view model can be any custom Java object/POJO. (Previous versions strictly required the model to be a Map).

There are two primary ways to instantiate a ModelAndView:

  • new ModelAndView(String view, Object model) (For POJOs)

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

Session

Sessions are accessible via:

  • sessionOrNull(): Finds and returns an existing session (or returns null).

  • session(): Finds an existing session or creates a new one if none exists.

Sessions are commonly used for authentication, storing user preferences, or tracking user state.

A session attribute must be a String or a primitive. The session API does not allow storing arbitrary Java objects or complex object graphs. It is intended as a simple, lightweight mechanism to store basic data.

Jooby provides the following SessionStore implementations:

  • In-Memory Sessions: Suitable for single-instance applications (or multi-instance if combined with a sticky-session proxy).

  • Signed Cookie Sessions: Stateless sessions signed with a secret key.

  • JWT (JSON Web Token) Sessions: Stateless, token-based sessions.

Note: Since Jooby 4.0.0, no session store is configured by default. Attempting to access a session at runtime without first configuring a store will result in an exception.

In-Memory Session

The in-memory session store saves session data directly in the server’s RAM. It uses a cookie or HTTP header solely to track the session ID.

In-Memory Session
Java
Kotlin
{
  setSessionStore(SessionStore.memory(Cookie.session("myappid")));

  get("/", ctx -> {
    Session session = ctx.session();    // 1
    session.put("foo", "bar");          // 2

    return session.get("foo").value();  // 3
  });
}
  1. Finds an existing session or creates a new one.

  2. Sets a session attribute.

  3. Gets a session attribute.

By default, the session ID is retrieved from a request cookie. The default session cookie never expires and is set to HttpOnly under the root / path.

To customize the 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. Configures an in-memory session store using a custom cookie named SESSION.

Alternatively, you can use an HTTP header to transmit the session token/ID instead of a cookie:

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. The Session Token/ID is read from the TOKEN HTTP header.

You can also combine both methods, telling Jooby to check the cookie first, and then fall back to the header:

Mixed Tokens
Java
Kotlin
{
  setSessionStore(SessionStore.memory(
      SessionToken.combine(SessionToken.cookie("SESSION"), SessionToken.header("TOKEN"))  // 1
  ));

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

    return session.get("foo").value();
  });
}
  1. The Session Token/ID is read from the SESSION cookie or the TOKEN header (in that order).

Signed Session

This is a stateless session store. The server does not keep any session state in memory. Instead, the entire session payload is serialized, cryptographically signed, and sent back and forth between the client and server on every request.

  • Session data is retrieved/saved entirely from/into the HTTP Cookie or Header.

  • Session data is signed using HmacSHA256 to prevent tampering. The secret key must be at least 256 bits long (32 bytes).

Signing and verification are handled internally using sign(String,String) and unsign(String,String).

Usage
Java
Kotlin
{
  String secret = "super-secret-key-must-be-32-bytes";                         // 1
  setSessionStore(SessionStore.signed(Cookie.session("myappid"), secret));     // 2

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

    return session.get("foo").value();
  });
}
  1. A secure, 32-byte secret key is required to sign the data.

  2. Creates a signed session store using a cookie and the secret key.

Just like the in-memory store, the signed session store also supports HTTP headers:

Signed Session with HTTP Headers
Java
Kotlin
{
  String secret = "super-secret-key-must-be-32-bytes";                            // 1
  setSessionStore(SessionStore.signed(SessionToken.header("TOKEN"), secret));     // 2

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

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

Additional Stores

In addition to the built-in memory and signed stores, Jooby provides external module integrations:

  • Caffeine: High-performance in-memory session store using the Caffeine cache library.

  • JWT: Stateless JSON Web Token session store.

  • Redis: Distributed session store using Redis.

Server-Sent Events

Server-Sent Events (SSE) is a mechanism that allows the server to push data to the client once a connection is established. Unlike WebSockets, SSE is strictly unidirectional: the server can send data to the client, but not the other way around.

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

  2. Send a message to the client.

Message Options

Additional message properties (like custom events, IDs, and retry timeouts) are available via the ServerSentMessage class:

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

For details on how these options are interpreted by the browser, see the MDN documentation on the Event stream format.

Connection Lost

The sse.onClose(Runnable) callback allows you to clean up and release resources when the connection ends. A connection is considered closed when you explicitly call sse.close() or when the remote client disconnects.

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

Keep Alive

You can use the keep-alive feature to prevent idle connections from timing out or being dropped by intermediate proxies:

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

This example sends a : message (an empty SSE comment) every 15 seconds to keep the connection active. If the client drops the connection, the sse.onClose event will be fired.

This feature is especially useful for quickly detecting closed connections without having to wait until your application tries to send a real event. (However, if your application already pushes data frequently—e.g., every few seconds—enabling keepAlive is generally unnecessary).

WebSockets

WebSockets are added using the ws() method:

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) -> {
      // Clean up resources // 4
    });

    configurer.onError((ws, cause) -> {
      // Handle exceptions // 5
    });
  });
}
  1. Register a WebSocket handler.

  2. On connection (open), send a message back to the client. This is also a good place to initialize resources.

  3. On receiving a new message, send a response back to the client.

  4. The WebSocket is about to close. You must free/release any acquired resources here.

  5. The WebSocket encountered an exception. Useful for logging the error or providing an alternative response if the socket is still open.

You are free to access the HTTP context from the WebSocket configurer or callbacks, but it is forbidden to modify the HTTP context or produce an HTTP response from it.

Accessing 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 a path variable (key).

  2. Access a session variable (foo).

Structured Data

Structured data (like JSON) is supported using the Value API and the render(Object) method.

To use structured messages, you need a registered MessageDecoder and MessageEncoder. In the following example, both are provided by the JacksonModule.

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

{
  install(new JacksonModule());                         // 1

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

  2. Parse and decode the incoming message to a MyObject.

  3. Encode myobject as JSON and send it to the client.

Alternatively, you can explicitly tell the WebSocket which decoder/encoder to use by specifying the consumes and produces attributes:

Explicit Content Types
Java
Kotlin
import io.jooby.jackson.JacksonModule;

{
  install(new JacksonModule());                         // 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);
}

Options

Connection Timeouts

Jooby automatically times out idle connections that have no activity after 5 minutes. You can control this behavior by setting the websocket.idleTimeout property in your configuration file:

application.conf
websocket.idleTimeout = 1h

See the Typesafe Config documentation for the supported duration format.

Max Size

The maximum message size is set to 128K by default. You can override it using the websocket.maxSize property:

application.conf
websocket.maxSize = 128K

See the Typesafe Config documentation for the supported size in bytes format.

Ecosystem

Extend the power of Jooby through its rich ecosystem of modules and standards. Learn how to seamlessly integrate with OpenAPI 3 to automatically generate interactive documentation and client SDKs, and explore a wide array of community and first-party modules that bring database access, security, and messaging to your application with minimal configuration.

The Jooby ecosystem is built on three core, interconnected concepts:

  1. Services: The objects and dependencies your application needs to run.

  2. Extensions: The mechanism for packaging and registering those services, along with routes and configuration.

  3. Modules: Pre-built extensions provided by Jooby to integrate popular third-party libraries.

Services and the Registry

At its core, Jooby uses a simple, built-in map called the ServiceRegistry to manage application state and dependencies.

Services can be registered as immediate singletons, or their lifecycle can be customized by registering a jakarta.inject.Provider.

You can explicitly put and retrieve services from the registry:

Basic Service Registry
Java
Kotlin
import jakarta.inject.Provider;

{
  // 1. Put a singleton service into the registry
  getServices().put(MyDatabase.class, new MyDatabase());

  // 2. Put a provider to customize lifecycle (e.g., prototype/lazy creation)
  getServices().put(MyService.class, (Provider<MyService>) () -> new MyService());

  get("/", ctx -> {
    // 3. Require the service at runtime
    MyDatabase db = require(MyDatabase.class);
    MyService service = require(MyService.class);

    return db.query();
  });
}

Collections of Services

The registry also supports grouping multiple services of the same type using Lists, Sets, or Maps.

Collections
Java
Kotlin
import io.jooby.Reified;

{
  // Add to a List
  getServices().listOf(Animal.class).add(new Cat());
  getServices().listOf(Animal.class).add(new Dog());

  // Add to a Map
  getServices().mapOf(String.class, Animal.class).put("cat", new Cat());

  get("/list", ctx -> {
    // Retrieve the List using the Reified type helper
    List<Animal> animals = ctx.require(Reified.list(Animal.class));
    return animals;
  });
}

Dependency Injection (DI) Bridge

While the ServiceRegistry acts as a simple service locator out-of-the-box, its true power lies in its ability to bridge to full Dependency Injection frameworks.

When you install a DI module (like Guice, Dagger, or Avaje Inject), the require() method seamlessly delegates to the underlying DI container. This allows you to use standard jakarta.inject.Inject annotations on your controllers and services, while still falling back to the Jooby registry when needed.

Extensions

The Extension API is how you package and distribute configuration, infrastructure, and services. It is a simple way of reusing code and decoupling technical features from your business logic.

Writing a Custom Extension

Let’s develop a custom extension that configures a DataSource service, registers it, and ensures it closes when the application shuts down.

Java
Kotlin
import io.jooby.Extension;
import io.jooby.Jooby;

public class MyExtension implements Extension {
   @Override
   public void install(Jooby app) {
      DataSource dataSource = createDataSource();          // 1

      app.getServices().put(DataSource.class, dataSource); // 2

      app.onStop(dataSource::close);                       // 3
   }

   private DataSource createDataSource() {
       // Initialization logic
   }
}
  1. Create the service.

  2. Save the service into the application’s service registry.

  3. Register a lifecycle hook to clean up the service when the application stops.

Now, you can install the extension in your main application and use the service:

Java
Kotlin
public class App extends Jooby {
   {
     install(new MyExtension());                       // 1

     get("/", ctx -> {
       DataSource ds = require(DataSource.class);      // 2
       // Use the datasource...
       return "Success";
     });
   }
}
  1. Install the custom extension.

  2. Retrieve the service that the extension registered.

Extensions are incredibly flexible. In addition to registering services, an extension can add standard routes, configure body decoders/encoders, or set up template engines.

Modules

Modules are simply built-in Extensions. They are thin layers that bootstrap and configure external third-party libraries (like HikariCP, Jackson, or Hibernate) using Jooby’s Extension API.

Unlike other frameworks, Jooby modules do not create new layers of abstraction or custom wrappers around the libraries they integrate. Instead, they expose the raw library components directly to your application via the Service Registry, allowing you to use the library’s native API exactly as its creators intended.

Modules are distributed as separate dependencies. Below is the catalog of officially supported Jooby modules:

Cloud

Data

Dependency Injection

Validation

Development Tools

  • Jooby Run: Run and hot reload your application.

  • Whoops: Pretty page stacktrace reporter.

  • Metrics: Application metrics from the excellent metrics library.

Event Bus

  • Camel: Camel module for Jooby.

  • Vertx: Vertx module for Jooby.

JSON

OpenAPI

Template Engine

Security

  • Jasypt: Encrypted configuration files.

  • Pac4j: Security engine module.

Session Store

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

  • JWT: JSON Web Token session store.

  • Redis: Save session data on redis.

Scheduler

Tooling and Operations

Streamline your development workflow with Jooby’s productivity suite. This section covers essential utilities like Hot Reload for instantaneous code changes without restarting the server, and deep integration with build systems like Maven and Gradle to manage your project’s lifecycle from the first line of code to the final deployment.

Configuration

Application configuration is built on the Typesafe Config library. By default, Jooby supports configuration provided in Java properties, JSON, or HOCON format.

Jooby allows you to override any property via system properties, environment variables, or program arguments.

Environment

The Environment class manages your application’s configuration and active environment names (e.g., dev, prod, test).

Environment names allow you to load different configuration files or toggle features (like caching or file reloading) depending on the deployment stage.

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

You can set active environment names in several ways:

  • Program Argument: java -jar myapp.jar prod,cloud (This works when using Jooby’s runApp methods).

  • System Property: java -Dapplication.env=prod -jar myapp.jar

  • Environment Variable: application.env=prod

Default Loading and Precedence

When you call getEnvironment(), Jooby searches for an application.conf file in the following order of priority:

  1. ${user.dir}/conf/application.conf (External file system)

  2. ${user.dir}/application.conf (External file system)

  3. classpath://application.conf (Internal jar resource)

Note

${user.dir} refers to the directory from which the JVM was launched. Jooby favors file system files over classpath files, allowing you to easily externalize configuration without rebuilding your jar.

Overrides

Properties are resolved using the following precedence (highest priority first):

  1. Program arguments (e.g., java -jar app.jar foo=bar)

  2. System properties (e.g., -Dfoo=bar)

  3. Environment variables (e.g., foo=bar java -jar app.jar)

  4. Environment-specific property file (e.g., application.prod.conf)

  5. Default property file (application.conf)

Accessing Properties
Java
Kotlin
{
  Environment env = getEnvironment();          // 1
  Config conf = env.getConfig();               // 2
  System.out.println(conf.getString("foo"));   // 3
}
  1. Retrieve the current environment.

  2. Access the underlying Config object.

  3. Extract the value for the key foo.

Multi-Environment Configuration

It is best practice to keep common settings in application.conf and override environment-specific values in separate files named application.[env].conf.

Example Structure
└── application.conf      (foo = "default", bar = "base")
└── application.prod.conf (foo = "production")

Running with java -jar myapp.jar prod results in: * foo: "production" (overridden) * bar: "base" (inherited from default)

To activate multiple environments, separate them with commas: java -jar app.jar prod,cloud.

Custom Configuration

If you want to bypass Jooby’s default loading logic, you can provide custom options or instantiate the environment manually.

Using Environment Options
Java
Kotlin
{
  setEnvironmentOptions(new EnvironmentOptions().setFilename("myapp.conf"));  // 1
}
  1. Loads myapp.conf instead of the default application.conf while maintaining standard precedence rules.

Direct Instantiation
Java
Kotlin
{
  Config conf = ConfigFactory.load("custom.conf");            // 1
  Environment env = new Environment(getClassLoader(), conf);  // 2
  setEnvironment(env);                                        // 3
}
  1. Manually load a configuration file.

  2. Wrap it in a Jooby Environment.

  3. Assign it to the application before startup.

Logging

Jooby uses SLF4J, allowing you to plug in your preferred logging framework.

Logback
  1. Add Dependency: logback-classic.

  2. Configure: Place logback.xml in your conf directory or classpath root.

Log4j2
  1. Add Dependencies: log4j-slf4j-impl and log4j-core.

  2. Configure: Place log4j2.xml in your conf directory or classpath root.

Environment-Aware Logging

Logging is also environment-aware. Jooby will look for logback.[env].xml or log4j2.[env].xml and favor them over the default files.

Important

To ensure environment-specific logging works correctly, avoid using static loggers in your main App class before runApp is called. Static loggers force the logging framework to initialize before Jooby can apply the environment-specific configuration. Use an instance logger or Jooby’s getLog() method instead.

Application Properties

Property Default Description

application.charset

UTF-8

Charset for encoding/decoding and templates.

application.env

dev

Active environment names. Jooby optimizes performance for non-dev environments.

application.lang

Locale.getDefault()

Supported languages for Context.locale().

application.tmpdir

tmp

Temporary directory for the application.

application.pid

System assigned

The JVM process ID.

See AvailableSettings for a complete reference.

Development

The jooby run tool provides a "hot reload" experience by restarting your application automatically whenever code changes are detected, without exiting the JVM. This makes Java and Kotlin development feel as fast and iterative as a scripting language.

The tool leverages JBoss Modules to efficiently reload application classes. It is available as both a Maven and a Gradle plugin.

Usage

1) Add the build plugin:

pom.xml
build.gradle
<plugins>
  <plugin>
    <groupId>io.jooby</groupId>
    <artifactId>jooby-maven-plugin</artifactId>
    <version>4.0.16</version>
  </plugin>
</plugins>

2) Configure the Main Class:

pom.xml
build.gradle
<properties>
  <application.class>myapp.App</application.class>
</properties>

3) Launch the Application:

Maven
Gradle
mvn jooby:run

Compilation and Restart

  • Source Files (.java, .kt): Changing a source file triggers an incremental compilation request. If the compilation succeeds, the application restarts automatically.

  • Configuration Files (.conf, .properties): Changes to these files trigger an immediate application restart without a compilation step.

  • Compilation Errors: Any errors during the build process are printed directly to the console by Maven or Gradle.

Note

For Eclipse users: The plugin detects the .classpath file in your project directory. If found, the plugin’s internal compiler is disabled, letting Eclipse handle the compilation while the plugin focuses on the restart logic.

Options

Below are the available configuration options with their default values:

pom.xml
build.gradle
<configuration>
  <mainClass>${application.class}</mainClass>
  <restartExtensions>conf,properties,class</restartExtensions>
  <compileExtensions>java,kt</compileExtensions>
  <port>8080</port>
  <waitTimeBeforeRestart>500</waitTimeBeforeRestart>
  <useSingleClassLoader>false</useSingleClassLoader>
</configuration>
  1. The application’s entry point (Main class).

  2. Extensions that trigger an immediate restart.

  3. Extensions that trigger a compilation followed by a restart.

  4. The local development port.

  5. The delay (in milliseconds) to wait after the last file change before restarting. Default is 500ms.

  6. If true, Jooby uses a single "fat" classloader. Set this to true if you encounter strange reflection or class-loading errors in complex projects. Since 3.x, Jooby uses a modular classloader by default for faster restarts and lower memory usage.

Testing with Classpath

To run the application while including the test scope/source set in the classpath, use the following commands:

  • Maven: mvn jooby:testRun

  • Gradle: ./gradlew joobyTestRun

Testing

Jooby provides dedicated tools for both lightweight unit testing and full-blown integration testing.

Unit Testing

Unit testing in Jooby is fast because it allows you to test your routes without starting a real HTTP server.

1) Add the Dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-test</artifactId>
  <version>4.0.16</version>
</dependency>

2) Define your Application:

App
Java
Kotlin
public class App extends Jooby {
  {
    get("/", ctx -> "Easy unit testing!");
  }
}

3) Write the Test:

Use the MockRouter to simulate requests and capture the return values of your handlers.

TestApp
Java
Kotlin
import io.jooby.test.MockRouter;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class TestApp {
  @Test
  public void test() {
    MockRouter router = new MockRouter(new App());
    assertEquals("Easy unit testing!", router.get("/").value());
  }
}
Checking Response Metadata

If your route modifies the context (like setting status codes or headers), you can verify the metadata using a callback:

Metadata Test
Java
Kotlin
@Test
public void testMetadata() {
  MockRouter router = new MockRouter(new App());
  router.get("/", response -> {
    assertEquals(StatusCode.OK, response.getStatusCode());
    assertEquals("Easy unit testing", response.value(String.class));
  });
}
Mocking the Context

For complex routes that interact with forms, bodies, or headers, you can provide a MockContext or a mock object from a library like Mockito.

Using MockContext
Java
Kotlin
@Test
public void testWithForm() {
  MockRouter router = new MockRouter(new App());
  MockContext context = new MockContext();

  context.setForm(Formdata.create(context).put("name", "Jooby"));

  assertEquals("Jooby", router.post("/", context).value());
}

Integration Testing

Integration tests run a real web server and allow you to test your application using any HTTP client. Jooby provides a JUnit 5 extension to manage the application lifecycle automatically.

  1. Add the Dependency:

  2. Write the Test: Annotate your test class with @JoobyTest.

Integration Test
Java
Kotlin
import io.jooby.test.JoobyTest;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

@JoobyTest(App.class)  // 1
public class IntegrationTest {

  static OkHttpClient client = new OkHttpClient();

  @Test
  public void testApp() throws IOException {
    Request request = new Request.Builder()
        .url("http://localhost:8911")  // 2
        .build();

    try (Response response = client.newCall(request).execute()) {
      assertEquals("Easy testing!", response.body().string());
    }
  }
}
  1. Jooby starts the application before the test and stops it afterward.

  2. The default integration test port is 8911.

Injecting Server Details

If you use a random port (port = 0) or want to avoid hardcoding URLs, you can inject server details directly into your test methods or fields:

Injection Example
Java
Kotlin
@JoobyTest(value = App.class, port = 0)
public void test(int serverPort, String serverPath) {
  // serverPort: e.g. 54321
  // serverPath: e.g. "http://localhost:54321"
}

Supported injectable types include: * int serverPort: The port the application is listening on. * String serverPath: The full base URL (e.g., http://localhost:port). * io.jooby.Environment: Access to the test environment. * com.typesafe.config.Config: Access to the application configuration. * io.jooby.Jooby: Access to the application instance itself.

Tip

When running integration tests, Jooby automatically sets the environment name to test. You can create a conf/application.test.conf file to provide test-specific settings.

Using a Factory Method

If your application requires constructor arguments, you can specify a factoryMethod to instantiate it:

Factory Method Test
Java
Kotlin
@JoobyTest(value = App.class, factoryMethod = "createApp")
public class TestApp {

  public static App createApp() {
    return new App("custom-argument");
  }
}

Server

Jooby supports multiple web server implementations. A server is automatically registered based on its presence on the project classpath.

Officially supported servers:

To use a specific server, add the corresponding dependency to your project:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-netty</artifactId>
  <version>4.0.16</version>
</dependency>
Important

Only one server dependency should be available on the classpath at a time.

Manual Setup

While Jooby usually loads the server automatically via the ServiceLoader API, you can also instantiate and configure a server manually in your main method.

This is particularly useful if you need to access server-specific features, such as configuring Project Loom (Virtual Threads) in Jetty:

Jetty with Virtual Threads
Java
Kotlin
import io.jooby.jetty.JettyServer;
import java.util.concurrent.Executors;
import org.eclipse.jetty.util.thread.QueuedThreadPool;

public static void main(String[] args) {
  var worker = new QueuedThreadPool();
  worker.setReservedThreads(0);
  worker.setVirtualThreadsExecutor(Executors.newVirtualThreadPerTaskExecutor());

  runApp(args, new JettyServer(worker), App::new);
}

Running Multiple Apps

Jooby servers can run multiple applications simultaneously.

Multi-App Server
Java
Kotlin
import io.jooby.netty.NettyServer;
import java.util.List;

public static void main(String[] args) {
  runApp(args, new NettyServer(), List.of(FooApp::new, BarApp::new));
}
Note

When running multiple apps, the server configuration (ports, threads, etc.) is determined by the first application setup that defines them.

Server Options

Server behavior can be controlled via the ServerOptions class or through application.conf.

Server Options
Java
Kotlin
{
  var options = new ServerOptions()
      .setPort(8080)
      .setIoThreads(16)
      .setWorkerThreads(64)
      .setGzip(false)
      .setMaxRequestSize(10485760) // 10MB
      .setHttp2(true);
}
Core Settings
  • server.port: The HTTP port (default: 8080). Use 0 for a random port.

  • server.ioThreads: Number of IO threads (Netty/Undertow). Defaults to Processors * 2.

  • server.workerThreads: Number of worker threads. Defaults to ioThreads * 8.

  • server.maxRequestSize: Maximum request size in bytes. Exceeding this triggers a 413 Request Entity Too Large response.

  • server.defaultHeaders: Automatically sets Date, Content-Type, and Server headers.

  • server.expectContinue: Enables support for 100-Continue requests.

HTTPS Support

Jooby supports HTTPS out of the box using either PKCS12 (default) or X.509 certificates.

Hello HTTPS (Self-Signed)

For development, you can enable a self-signed certificate with one line:

Self-Signed HTTPS
Java
Kotlin
public static void main(String[] args) {
  var options = new ServerOptions().setSecurePort(8443);
  runApp(args, new NettyServer(options), App::new);
}
Valid Certificates (X.509 & PKCS12)

For production, you should use valid certificates (e.g., from Let’s Encrypt). You can configure these in your code or via application.conf.

X.509 Configuration
server.ssl.type = X509
server.ssl.cert = "path/to/server.crt"
server.ssl.key = "path/to/server.key"
PKCS12 Configuration
server.ssl.type = PKCS12
server.ssl.cert = "path/to/server.p12"
server.ssl.password = "password"
Mutual TLS (Client Authentication)

To require clients to present a certificate, set the clientAuth mode to REQUIRED. This usually requires a Trust Store containing the certificates you trust.

Mutual TLS
Java
Kotlin
{
  var ssl = SslOptions.pkcs12("server.p12", "password")
    .setTrustCert(Paths.get("trust.crt"))
    .setClientAuth(SslOptions.ClientAuth.REQUIRED);

  var options = new ServerOptions().setSsl(ssl);
}

HTTP/2 Support

HTTP/2 is supported across all servers. To use it in a browser, you must enable HTTPS.

Enable HTTP/2
Java
Kotlin
{
  var options = new ServerOptions()
      .setHttp2(true)
      .setSecurePort(8443);
}

OpenSSL (Conscrypt)

By default, Jooby uses the JDK’s built-in SSL engine. For better performance and features (like TLS v1.3 on older Java versions), you can use the OpenSSL-backed Conscrypt provider.

Simply add the dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-conscrypt</artifactId>
  <version>4.0.16</version>
</dependency>

Packaging

This section describes the primary options for packaging and distributing your Jooby application.

Single Jar (Fat/Uber Jar)

The most common deployment option is creating a single executable "Fat Jar" that contains your application code along with all its dependencies.

Tip

The Jooby CLI automatically configures your project for single jar distribution. The examples below show how to configure it manually if needed.

Maven (Shade Plugin)
Gradle (Shadow Plugin)
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <version>3.6.1</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>

To build the package:

  • Maven: mvn clean package

  • Gradle: ./gradlew shadowJar

Stork

Stork is a specialized packaging, launch, and deployment tool for Java applications. It generates platform-specific native launchers (shell scripts or batch files) and organizes your dependencies in a clean directory structure.

Note

The Stork integration is currently only available for Maven projects.

To configure Stork:

  1. Create the configuration: Define a src/etc/stork/stork.yml file.

stork.yml
# Application name (no spaces)
name: "${project.artifactId}"
display_name: "${project.name}"

# Type of launcher (CONSOLE or DAEMON)
type: DAEMON
main_class: "${application.class}"

# Platforms to generate (LINUX, WINDOWS, MAC_OSX)
platforms: [ LINUX ]

# Directory mode: RETAIN (current) or APP_HOME (switch to app root)
working_dir_mode: RETAIN

# Runtime Requirements
min_java_version: "17"
min_java_memory: 512
max_java_memory: 512

# Create a symbolic link to java as "<app_name>-java" for easier process tracking
symlink_java: true
  1. Add the Maven Tiles plugin: Use the Jooby Stork tile to automate the build.

pom.xml
<build>
  <plugins>
    <plugin>
      <groupId>io.repaint.maven</groupId>
      <artifactId>tiles-maven-plugin</artifactId>
      <version>2.43</version>
      <extensions>true</extensions>
      <configuration>
        <tiles>
          <tile>io.jooby:jooby-stork:4.0.16</tile>
        </tiles>
      </configuration>
    </plugin>
  </plugins>
</build>
  1. Build the package: Run mvn package. The resulting Stork .zip file will be located in the target directory.

Appendix

Upgrading from 3.x to 4.x

You will find here notes/tips about how to migrate from 3.x to 4.x.

Note

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

Requirements

  • Java 21 as minimum

Special HTTP names

Starting from 4.0.7 @XXXParam the default annotation value attribute is actually that: the default value of the parameter. In previous version this was used it for invalid/special HTTP names.

In 3.x:

@QuerParam("some-http-name") String name

In 4.x

@QuerParam(name = "some-http-name") String name

The value is now reserved for default values:

@QueryParam("20") int pageSize

Buffer API

The package io.jooby.buffer is gone. It was replaced by io.jooby.output these classes are used mainly by the MessageEncoder API, the new API is easier to use and has better performance.

Value API

The new package is now io.jooby.value. The API is now decoupled from Context in future release will be the basis of a new configuration system.

Also, the io.jooby.ValueNode and io.jooby.ValueNodeConverter are gone.

Session API

For security reasons, the default HTTP session was removed. You need to configure the session explicitly and provide a cookie session name. The jooby.sid cookie name was removed from project.

Server configuration

The install(Server), setServerOptions, start() method are gone. With the new support for multiple applications in a single server, these methods are useless.

The new way:

New way to boot
runApp(args, new NettyServer(new ServerOptions()), App::new);

Packages

3.x

4.x

Module

io.jooby.buffer

io.jooby.output

replacement jooby (core)

Classes

3.x

4.x

Module

Description

io.jooby.buffer.*

-

jooby (core)

removed

io.jooby.output.*

jooby (core)

new output API

io.jooby.MvcFactory

-

jooby (core)

was deprecated and now removed

io.jooby.annotation.ResultType

-

jooby (core)

removed

io.jooby.ValueNode

io.jooby.value.Value

jooby (core)

replaced/merged

io.jooby.ValueNodeConverter

io.jooby.value.ValueConverter

jooby (core)

replaced/merged

io.jooby.RouteSet

io.jooby.Route.Set

jooby (core)

moved into Route and renamed to Set

Method

3.x

4.x

Description

io.jooby.Jooby.setServerOptions()

Server.setOptions()

removed in favor of Server.setOptions()

io.jooby.Router.mvc

-

it was deprecated and now removed

io.jooby.Router.decorator

-

it was deprecated and now removed

io.jooby.Router.getConverters

io.jooby.Router.getValueFactory

replaced

io.jooby.Router.getBeanConverters

io.jooby.Router.getValueFactory

replaced

io.jooby.Router.attribute(String)

Router.getAttribute(String)

Renamed

io.jooby.Router.RouteOption

io.jooby.RouterOptions

Moved to RouterOptions

io.jooby.Router.setTrustProxy

RouterOptions.setTrustProxy

Moved to RouterOptions

Upgrading from 2.x to 3.x

You will find here notes/tips about how to migrate from 2.x to 3.x.

Note

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

Requirements

  • Java 17 as minimum

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.

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

jakarta

2.x

3.x

javax.servlet

jakarta.servlet

javax.inject

jakarta.inject

javax.persistence

jakarta.persistence

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

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

Class renames

2.x

3.x

Module

io.jooby.Route.Decorator

jooby (core)

io.jooby.Route.Filter

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)

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

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

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.

Upgrading from 1.x to 2.x

You will find here notes/tips about how to migrate from 1.x to 2.x.

Maven coordinates

org.jooby became io.jooby. Hence, use <groupId>org.jooby</groupId> for all dependencies.

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

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)

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.