∞ 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.
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
-
Lightweight and Fast: See the TechEmpower Benchmark.
-
High Productivity: Built-in hot-reload for development.
-
Flexible Routing: Use the fluent Script/Lambda API or MVC annotations (Jooby or JAX-RS).
-
Modern Tooling: Full OpenAPI 3 support.
-
Execution Options: Choose between Event Loop or worker threads.
-
Reactive Ready: Support for reactive responses (CompletableFuture, RxJava, Reactor, Mutiny, and Kotlin Coroutines).
-
Extensible: Scale to a full-stack framework using extensions and modules.
|
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:
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:
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.
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
-
Download jooby-cli.zip.
-
Unzip the file into a preferred directory.
-
Locate the native launchers in the
binfolder.
|
Tip
|
Add the launcher ( |
|
Note
|
The |
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:
jooby> create myapp
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: |
jooby> create myapp --gradle
jooby> create myapp --gradle --kotlin
jooby> create myapp --mvc
jooby> create myapp --server undertow
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:
{
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:
Route
A Route consists of three parts:
{
// 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();
});
}
-
HTTP method/verb (e.g.,
GET,POST) -
Path pattern (e.g.,
/foo,/foo/{id}) -
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:
{
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.
{
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:
@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
valuemethod, the annotation’s name becomes the attribute name. -
Otherwise, the method name is used as the attribute name.
Path Pattern
Static
{
get("/foo", ctx -> "Foo");
}
Variable
{
// 1
get("/user/{id}", ctx -> {
int id = ctx.path("id").intValue(); // 2
return id;
});
}
-
Defines a path variable
id. -
Retrieves the variable
idas anint.
{
// 1
get("/file/{file}.{ext}", ctx -> {
String filename = ctx.path("file").value(); // 2
String ext = ctx.path("ext").value(); // 3
return filename + "." + ext;
});
}
-
Defines two path variables:
fileandext. -
Retrieves the string variable
file. -
Retrieves the string variable
ext.
{
// 1
get("/profile/{id}?", ctx -> {
String id = ctx.path("id").value("self"); // 2
return id;
});
}
-
Defines an optional path variable
id. The trailing?makes it optional. -
Retrieves the variable
idas aStringif present, or uses the default value:self.
The trailing ? makes the path variable optional. The route matches both:
-
/profile -
/profile/eespina
Regex
{
// 1
get("/user/{id:[0-9]+}", ctx -> {
int id = ctx.path("id").intValue(); // 2
return id;
});
}
-
Defines a path variable
id. The regex expression is everything after the first:(e.g.,[0-9]+). -
Retrieves the
intvalue.
The optional syntax is also supported for regex path variables (e.g., /user/{id:[0-9]+}?). This matches:
-
/user -
/user/123
* Catchall
{
// 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;
});
}
-
The trailing
*defines acatchallpattern. -
We access the
catchallvalue using the*character. -
In this example, we named the
catchallpattern and access it using thepathvariable name.
|
Note
|
A |
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 |
{
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:
-
GET /user/ppicapiedra⇒ppicapiedra -
GET /user/me⇒my profile -
Unreachable ⇒ overridden by the next route.
-
GET /users⇒new users(notusers).
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);
}
{
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";
});
}
-
Saves the start time.
-
Proceeds with execution (the pipeline).
-
Computes and prints latency.
-
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);
}
{
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);
}
{
after((ctx, result, failure) -> {
System.out.println(result); // 1
ctx.setResponseHeader("foo", "bar"); // 2
});
get("/", ctx -> {
return "Jooby";
});
}
-
Prints
Jooby. -
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.
{
after((ctx, result, failure) -> {
System.out.println(result); // 1
ctx.setResponseHeader("foo", "bar"); // 2
});
get("/", ctx -> {
return ctx.send("Jooby");
});
}
-
Prints
null(no value). -
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():
{
after((ctx, result, failure) -> {
if (ctx.isResponseStarted()) {
// Do not modify response
} else {
// Safe to modify response
}
});
}
|
Note
|
An |
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.
{
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.
{
after((ctx, result, failure) -> {
if (failure instanceof MyBusinessException) {
ctx.send("Recovering from something"); // 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.
{
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
});
}
-
Will be
OriginalException. -
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).
{
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);
});
}
-
Attaches a completion listener.
-
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:
{
// 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⇒3 -
/2⇒4
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 In Jooby 1.x, the Jooby 1.x
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 |
Order
Order follows a what you see is what you get approach. Routes are stacked in the order they are defined.
{
// 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⇒2 -
/2⇒4
Scoped Filter
The routes(Runnable) and path(String,Runnable) operators group one or more routes.
A scoped filter looks like this:
{
// 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:
-
Introduces a new scope via the
routesoperator. -
/4⇒9 -
/1⇒2
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.
{
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.
{
path("/api/user", () -> { // 1
get("/{id}", ctx -> ...); // 2
get("/", ctx -> ...); // 3
post("/", ctx -> ...); // 4
// ...
});
}
-
Sets the common prefix
/api/user. -
GET /api/user/{id} -
GET /api/user -
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:
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
}
}
-
Imports all routes from
Foo. Output:/foo⇒/foo -
Imports all routes from
Bar. Output:/bar⇒/bar -
Adds more routes. Output:
/app⇒/app
public class Foo extends Jooby {
{
get("/foo", Context::getRequestPath);
}
}
public class App extends Jooby {
{
mount("/prefix", new Foo()); // 1
}
}
-
Now all routes from
Foowill 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:
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
}
}
-
Imports all routes, services, callbacks, etc. from
Foo. Output:/foo⇒/foo -
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:
{
Foo foo = new Foo();
install(() -> foo); // Won't work
}
The Foo application must be lazily initialized:
{
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:
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:
-
/api⇒v1; whenversionheader isv1 -
/api⇒v2; whenversionheader isv2
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.
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 |
Options
Routing
The setRouterOptions(RouterOptions) method controls routing behavior.
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 |
|---|---|---|---|
|
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. |
|
boolean |
false |
Indicates whether the routing algorithm uses case-sensitive matching for incoming request paths. |
|
boolean |
false |
Indicates whether trailing slashes are ignored on incoming request paths. |
|
boolean |
false |
Throws an exception if multiple routes are registered with the same HTTP method and path pattern. |
|
boolean |
false |
Normalizes incoming request paths by replacing multiple consecutive slashes with a single slash. |
|
boolean |
true |
Indicates whether response headers should be cleared/reset when an exception occurs. |
|
boolean |
false |
When true, parses |
|
Important
|
trustProxy: This should only be enabled if your application is running behind a reverse proxy configured to send |
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.
<form method="post" action="/form">
<input type="hidden" name="_method" value="put">
</form>
import io.jooby.Jooby;
...
{
setHiddenMethod("_method"); // 1
put("/form", ctx -> { // 2
return "Updated!";
});
}
-
Configures the router to look for a form field named
_method. -
Executes the
PUThandler for/forminstead of the standardPOST.
(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:
import io.jooby.Jooby;
...
{
setHiddenMethod(ctx -> ctx.header("X-HTTP-Method-Override").toOptional()); // 1
}
-
Overrides the HTTP method by extracting it from the
X-HTTP-Method-Overriderequest 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:
{
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.lang = en, en-GB, de
Or configure it programmatically using setLocales(List):
{
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.
{
get("/", ctx -> {
String token = ctx.header("token").value(); // 1
Value headers = ctx.headers(); // 2
Map<String, String> headerMap = ctx.headerMap(); // 3
// ...
});
}
-
Retrieves the header
token. -
Retrieves all headers as a Value.
-
Retrieves all headers as a Map.
Cookie
Request cookies are sent to the server via the Cookie header. Jooby provides simple key/value access:
{
get("/", ctx -> {
String token = ctx.cookie("token").value(); // 1
Map<String, String> cookieMap = ctx.cookieMap(); // 2
// ...
});
}
-
Retrieves the cookie named
token. -
Retrieves all cookies as a Map.
Path
Path parameters are part of the URI. Use the {identifier} notation to define a path variable.
{
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
}
-
Standard path variable
id. -
Path variable
idprefixed with@. -
Multiple variables:
nameandext. -
Unnamed catchall path variable.
-
Path variable strictly matching a regular expression.
{
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
// ...
});
}
-
Access the
rawpath string (e.g.,/a breturns/a%20b). -
Path as a Value object (decoded).
-
Path as a
Map<String, String>(decoded). -
Specific path variable
nameas aString(decoded).
Query
The query string is the part of the URI that starts after the ? character.
{
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; }
}
-
Access the
rawquery string (e.g.,?q=a%20b). -
Query string as a QueryString object (e.g.,
{q=a b}). -
Query string as a multi-value map (e.g.,
{q=[a b]}). -
Access decoded variable
q. Throws a400 Bad Requestif missing. -
Binds the query string directly to a
SearchQueryobject.
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.
{
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;
}
}
-
Form as Formdata.
-
Form as a multi-value map.
-
Specific form variable
id. -
Specific form variable
pass. -
Form automatically bound to a
Userobject.
Multipart & File Uploads
Multipart data is sent in the HTTP body and encoded as multipart/form-data. It is required for file uploads.
{
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;
}
}
-
Form as Multipart.
-
Form as a multi-value map.
-
Specific multipart text variable
id. -
Specific multipart text variable
pass. -
Single file upload named
pic. -
Multipart form bound to a
Userobject (including the file).
|
Note
|
File Upload
File uploads are only available for multipart requests. Java Kotlin
|
Session
Session parameters are available via session() or sessionOrNull(). (See the full Session Chapter for details).
Session session = ctx.session(); // 1
String attribute = ctx.session("attribute").value(); // 2
-
Finds an existing Session or creates a new one.
-
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.
get("/", ctx -> {
return ctx.flash("success").value("Welcome!"); // 3
});
post("/save", ctx -> {
ctx.flash().put("success", "Item created"); // 1
return ctx.sendRedirect("/"); // 2
});
-
Sets a flash attribute:
success. -
Redirects to the home page.
-
Displays the flash attribute
success(if it exists) or defaults toWelcome!.
Flash attributes are implemented using an HTTP Cookie. To customize the cookie (the default name is jooby.flash), use setFlashCookie(Cookie).
{
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().
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.
get("/{foo}", ctx -> {
List<Certificate> certificates = ctx.getClientCertificates(); // 1
Certificate peerCertificate = certificates.get(0); // 2
});
-
Get all certificates presented by the client during the SSL handshake.
-
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:
{
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:
-
Access parameter
nameasString:-
/?name=foo⇒foo -
/⇒Bad Request(400): Missing value: "name"
-
-
Access parameter
scoreasfloat:-
/?score=1⇒1.0 -
/?score=string⇒Bad Request(400)(Type mismatch) -
/⇒Bad Request(400)
-
-
Access parameter
enabledasboolean:-
/?enabled=true⇒true -
/?enabled=string⇒Bad Request(400) -
/⇒Bad Request(400)
-
-
Access parameter
decimaland convert it to a custom type (BigDecimal):-
/?decimal=2.3⇒2.3 -
/?decimal=string⇒Bad Request(400) -
/⇒Bad Request(400)
-
Default and Optional value
You can handle optional parameters by providing a default value or requesting an Optional object:
{
get("/search", ctx -> {
String q1 = ctx.query("q").value("*:*"); // 1
Optional<String> q2 = ctx.query("q").toOptional(); // 2
return q1;
});
}
-
Retrieve variable
qas aStringwith a fallback default value of:.-
/search?q=foo⇒foo -
/search⇒:
-
-
Retrieve variable
qwrapped in anOptional<String>:-
/search?q=foo⇒Optional[foo] -
/search⇒Optional.empty
-
Multiple values
Multiple values for a single parameter key can be retrieved as Collections:
{
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
// ...
});
}
-
Parameter
qasList<String>:-
/⇒[] -
/?q=foo⇒[foo] -
/?q=foo&q=bar⇒[foo, bar]
-
-
Parameter
nasList<Integer>:-
/⇒[] -
/?n=1&n=2⇒[1, 2]
-
-
Parameter
dmapped toList<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
{
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
// ...
});
}
-
Retrieves the
usernode. -
Extracts
namefrom theusernode. -
Extracts
passfrom theusernode. -
Safely extracts
emailwith 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:
?member.firstname=Pedro&member.lastname=Picapiedra
?member[firstname]=Pedro&member[lastname]=Picapiedra
?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-urlencodedForm data -
multipart/form-dataForms
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
{
get("/", ctx -> {
Member member = ctx.query(Member.class);
// ...
});
}
Binding a nested Member from a root node:
/?member.firstname=Pedro&member.lastname=Picapiedra
{
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
{
get("/", ctx -> {
List<Member> members = ctx.query().toList(Member.class);
// ...
});
}
Binding complex nested hierarchies:
/?id=flintstones&members[0]firstname=Pedro&members[0]lastname=Picapiedra
{
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:
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.
{
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:
{
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
// ...
});
}
-
Reads the HTTP body as a
String. -
Reads the HTTP body as a
byte array. -
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.
{
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;
});
}
-
Initialize your favorite JSON library.
-
Register the decoder to trigger when the
Content-Typeheader matchesapplication/json. -
Read the raw body as a
byte[]. -
Parse the payload into the requested type.
-
Inside the route, calling
ctx.body(Type)automatically triggers the registered decoder.
Response Body
The response body is generated by the route handler.
{
get("/", ctx -> {
ctx.setResponseCode(200); // 1
ctx.setResponseType(MediaType.text); // 2
ctx.setResponseHeader("Date", new Date()); // 3
return "Response"; // 4
});
}
-
Set the status code to
200 OK(this is the default). -
Set the
Content-Typetotext/plain(this is the default for strings). -
Set a custom response header.
-
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).
{
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
});
}
-
Initialize your favorite JSON library.
-
Register the encoder to trigger when the client’s
Acceptheader matchesapplication/json. -
Convert the route’s result into JSON.
-
Set the
Content-Typeheader toapplication/json. -
Return the encoded JSON payload.
-
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
{
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
@JsonViewcan 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 theidand specific fields from the nestedaddressobject. -
Wildcards:
(id, address(*))selects theidand all available fields within theaddressobject. -
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
@Projectannotation. 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 |
Streaming / Chunked
The Streaming/Chunked API is available via:
-
responseStream(): A blocking API that provides an
OutputStream. -
responseWriter(): A blocking API that provides a
PrintWriter. -
responseSender(): A non-blocking API that provides a Sender.
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.
{
get("/chunk", ctx -> {
try(Writer writer = ctx.responseWriter()) { // 1
writer.write("chunk1"); // 2
// ...
writer.write("chunkN");
}
return ctx; // 3
});
}
-
Get the
Writerinside a try-with-resources (orusein Kotlin) block so it closes automatically. -
Write chunks of data.
-
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:
{
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.
{
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
});
}
-
Send a download from a
Pathas an attachment. -
Send a download from an
InputStreaminline.
Alternatively, you can use the static builder methods on FileDownload and specify the download type later.
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.
{
mode(EVENT_LOOP); // 1
use(ReactiveSupport.concurrent()); // 2
get("/non-blocking", ctx -> {
return CompletableFuture // 3
.supplyAsync(() -> {
// ... // 4
});
});
}
-
The application runs in the event loop.
-
Indicates we want to go non-blocking and handle CompletableFuture responses.
-
The value is provided from the event loop. No blocking code is permitted.
-
The value is computed asynchronously.
Running your application in worker mode works identically, except you are allowed to make blocking calls:
{
mode(WORKER); // 1
use(ReactiveSupport.concurrent()); // 2
get("/blocking", ctx -> {
return CompletableFuture // 3
.supplyAsync(() -> {
// ... // 4
});
});
}
-
The application runs in worker mode.
-
Indicates we want to go non-blocking.
-
The value is provided from the worker thread. Blocking code is permitted.
-
The value is computed asynchronously.
The default mode mimics the event loop mode when a route produces a non-blocking type:
{
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., |
CompletableFuture
CompletableFuture is a non-blocking type that produces a single result:
{
use(ReactiveSupport.concurrent());
get("/non-blocking", ctx -> {
return CompletableFuture
.supplyAsync(() -> "Completable Future!")
.thenApply(it -> "Hello " + it);
});
}
Mutiny
1) Add the SmallRye Mutiny dependency:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-mutiny</artifactId>
<version>4.0.16</version>
</dependency>
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);
});
}
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:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-rxjava3</artifactId>
<version>4.0.16</version>
</dependency>
import io.jooby.rxjava3.Reactivex;
{
use(Reactivex.rx());
get("/non-blocking", ctx -> {
return Single
.fromCallable(() -> "Single")
.map(it -> "Hello " + it);
});
}
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:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-reactor</artifactId>
<version>4.0.16</version>
</dependency>
import io.jooby.Reactor;
{
use(Reactor.reactor());
get("/non-blocking", ctx -> {
return Mono
.fromCallable(() -> "Mono")
.map(it -> "Hello " + it);
});
}
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 {
get("/") {
delay(100) // 1
"Hello Coroutines!" // 2
}
}
}
-
Call a suspending function.
-
Send the response to the client.
{
coroutine {
get("/") {
ctx.doSomething() // 1
}
}
}
suspend fun Context.doSomething(): String {
delay(100) // 2
return "Hello Coroutines!" // 3
}
-
Call an extension suspending function.
-
Safely perform a suspending or blocking call.
-
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:
{
worker(Executors.newCachedThreadPool())
coroutine {
get("/") {
val n = 5 * 5 // 1
delay(100) // 2
"Hello Coroutines!" // 3
}
}
}
-
Statement runs in the worker executor (cached thread pool).
-
Calls a suspending function.
-
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:
{
coroutine(CoroutineStart.UNDISPATCHED) {
get("/") {
val n = 5 * 5 // 1
delay(100) // 2
"Hello Coroutines!" // 3
}
}
}
-
Statement runs in the event loop (caller thread).
-
Calls a suspending function and dispatches to the worker executor.
-
Produces a response from the worker executor.
You can also extend the CoroutineContext in which the routes run:
{
coroutine {
launchContext { MDCContext() } // 1
get("/") {
// ...
}
}
}
-
The
launchContextlambda runs before launching each coroutine. It allows you to customize theCoroutineContextfor 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.
{
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).
import io.jooby.handler.AccessLogHandler;
{
use(new AccessLogHandler()); // 1
get("/", ctx -> "Logging...");
}
-
Install the
AccessLogHandleras 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:
-
requestHeader(String…): Appends request headers to the log line.
-
responseHeader(String…): Appends response headers to the log line.
|
Tip
|
If your application runs behind a reverse proxy (like Nginx or AWS ALB) that sends |
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.
import io.jooby.handler.CorsHandler;
{
use(new CorsHandler()); // 1
path("/api", () -> {
// API methods
});
}
-
Install the
CorsHandlerwith default options.
The default configuration is:
-
Origin:
*(All origins) -
Credentials:
true -
Methods:
GET,POST -
Headers:
X-Requested-With,Content-Type,Accept, andOrigin -
Max Age:
30m
Customizing CORS Options
To customize these settings, use the Cors class:
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
}
-
Specify the allowed HTTP methods.
-
Pass the custom configuration to the
CorsHandler.
Configuration-based CORS
You can also define your CORS settings directly in your application.conf file:
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:
import io.jooby.handler.Cors;
import io.jooby.handler.CorsHandler;
{
Cors cors = Cors.from(getConfig()); // 1
use(new CorsHandler(cors));
}
-
Loads the CORS options defined in the
corsnamespace 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.
<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.
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:
-
HTTP Header:
X-CSRF-Tokenorcsrf -
Cookie:
csrf -
Form Parameter:
csrf
Customization
You can customize the behavior of the handler using the following methods:
-
setTokenGenerator(Function): Customize how tokens are generated (defaults to a random UUID).
-
setRequestFilter(Predicate): Define which requests should be validated. By default, it validates
POST,PUT,PATCH, andDELETErequests.
|
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.
import io.jooby.GracefulShutdown;
{
install(new GracefulShutdown()); // 1
get("/", ctx -> "Hello!");
}
-
Install the
GracefulShutdownextension.
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.
import io.jooby.handler.HeadHandler;
{
use(new HeadHandler()); // 1
get("/", ctx -> "Full response body");
}
-
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 |
RateLimitHandler
The RateLimitHandler provides request throttling using the popular Bucket4j library.
To use this handler, add the following dependency to your project:
<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.
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
}
-
Create a bucket with the desired capacity and refill rate.
-
Install the
RateLimitHandleras abeforefilter.
Throttling by Key
Often, you want to limit requests per user, API key, or IP address rather than globally.
{
before(new RateLimitHandler(remoteAddress -> {
Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
return Bucket4j.builder().addLimit(limit).build();
}));
}
{
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.
{
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.
import io.jooby.handler.SSLHandler;
{
before(new SSLHandler()); // 1
get("/", ctx -> "You are on: " + ctx.getScheme());
}
-
Install the
SSLHandleras abeforefilter.
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:
{
// 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 |
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.
import io.jooby.handler.TraceHandler;
{
use(new TraceHandler()); // 1
get("/", ctx -> "Hello World");
}
-
Install the
TraceHandleras 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
|
|
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.
Not Found message: Page not found status code: 404
{
"message": "Page not found",
"status": 404,
"reason": "Not Found"
}
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:
{
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
});
}
-
Register a global (catch-all) exception handler.
-
Log the error.
-
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:
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
}
});
}
-
Install a template engine module.
-
Install a JSON module.
-
Check if the
Acceptheader prefersapplication/json. -
Render the JSON response if matched.
-
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:
import static io.jooby.StatusCode.NOT_FOUND;
{
error(NOT_FOUND, (ctx, cause, statusCode) -> {
ctx.send(statusCode); // 1
});
}
-
Send a silent
404response 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:
{
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:
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:
problem.details {
enabled = true
log4xxErrors = true // 1
muteCodes = [401, 403] // 2
muteTypes = ["com.example.MyMutedException"] // 3
}
-
By default, only server errors (
5xx) are logged. You can enable logging for client errors (4xx) as well. (If your logger is set toDEBUG, the log will also include the stack trace). -
Mute logging entirely for specific HTTP status codes.
-
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:
-
valueOf(StatusCode): Derives the title from the status code.
-
valueOf(StatusCode,String): Specifies a custom
title. -
valueOf(StatusCode,String,String): Specifies a custom
titleanddetail.
There are also shorthands for common HTTP errors:
-
HttpProblem.badRequest(…)(400) -
HttpProblem.notFound(…)(404) -
HttpProblem.unprocessableEntity(…)(422) -
HttpProblem.internalServerError()(500)
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:
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:
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:
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:
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:
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:
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:
{
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
});
}
-
Transform the custom exception into an
HttpProblem. -
Propagate the problem back to the global
ProblemDetailsHandlerto ensure standard rendering.
|
Important
|
Do not attempt to render |
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).
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):
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:
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.
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:
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 |
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
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);
}
}
-
CompletableFutureis a non-blocking type; this route executes directly on the event loop. -
Stringis 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 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(orDEFAULTreturning a blocking type). -
The application mode is set to
EVENT_LOOPand an explicitdispatchblock 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:
{
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:
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.
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);
}
}
-
Set a base path pattern. The
@Pathannotation can be applied at the class or method level. -
Define the HTTP method.
-
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:
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:
public class MyController {
@GET
public String handle(@HeaderParam String token) { // 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:
public class MyController {
@GET
public String handle(@HeaderParam("Last-Modified-Since") long lastModifiedSince) {
// ...
}
}
Cookie
Extract cookies using the CookieParam annotation:
public class MyController {
@GET
public String handle(@CookieParam String token) { // 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:
public class MyController {
@Path("/{id}")
public String handle(@PathParam String id) {
// ...
}
}
Query
Extract query string variables using the QueryParam annotation:
public class MyController {
@Path("/")
public String handle(@QueryParam String q) {
// ...
}
}
Formdata / Multipart
Extract form-data or multipart parameters using the FormParam annotation:
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:
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.
public class Controller {
@GET("/{foo}")
public String bind(@BindParam MyBean bean) {
return "with custom mapping: " + bean;
}
}
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:
public class MyController {
@GET
public String handle(@FlashParam String success) { // 1
// ...
}
}
-
Accesses the flash attribute named
success.
Session
Extract specific session attributes using the SessionParam annotation:
public class MyController {
@GET
public String handle(@SessionParam String userId) { // 1
// ...
}
}
-
Accesses the session attribute named
userId.
You can also request the entire Session object:
public class MyController {
@GET
public String handle(Session session) { // 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:
public class MyController {
@GET
public String handle(@ContextParam String userId) { // 1
// ...
}
}
-
Accesses the context attribute named
userId.
You can also request all attributes at once:
public class MyController {
@GET
public String handle(@ContextParam Map<String, Object> attributes) { // 1
// ...
}
}
-
To retrieve all context attributes, the parameter must be typed as a
Map<String, Object>(orMap<String, Any>in Kotlin).
Multiple Sources
Use the Param annotation to search for a parameter across multiple sources with an explicitly defined fallback order:
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:
@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:
@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:
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:
public class MyController {
@GET("/blocking")
@Dispatch // 1
public String blocking() {
return "I'm blocking";
}
}
-
Forces the route to run in the
WORKERexecutor, safely allowing blocking calls.
The Dispatch annotation also supports routing execution to a named, custom executor:
public class MyController {
@GET("/blocking")
@Dispatch("single") // 1
public String blocking() {
return "I'm blocking";
}
}
-
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:
{
executor("single", Executors.newSingleThreadExecutor());
mvc(new MyController_());
}
JAX-RS Annotations
Alternatively, you can use JAX-RS annotations to define MVC routes.
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 |
|---|---|---|---|
|
boolean |
true |
Runs the annotation processor in debug mode. |
|
boolean |
true |
Hints to Maven/Gradle to perform incremental compilation. Essential for fast development iteration. |
|
array |
[] |
A comma-separated list of annotations to skip during bytecode generation (i.e., do not attach them as route attributes). |
|
boolean |
false |
Sets the |
|
string |
Adds a prefix to the generated class name. |
|
|
string |
_ |
Sets the suffix for the generated class name. |
Setting Options
<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 |
Templates
Templates are rendered using a ModelAndView object and require a TemplateEngine implementation to be installed.
{
install(new MyTemplateEngineModule()); // 1
get("/", ctx -> {
MyModel model = new MyModel(); // 2
return new ModelAndView("index.html", model); // 3
});
}
-
Install a template engine module.
-
Build the view model.
-
Return a
ModelAndViewinstance containing the template path and the model data.
You can explicitly set the desired locale for template rendering on the ModelAndView object:
{
install(new MyTemplateEngineModule());
get("/", ctx -> {
MyModel model = new MyModel();
return new ModelAndView("index.html", model)
.setLocale(Locale.GERMAN); // 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:
{
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
});
}
-
Install Handlebars.
-
Install Freemarker.
-
Renders using Handlebars due to the
.hbsextension. -
Renders using Freemarker due to the
.ftlextension.
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.
{
setSessionStore(SessionStore.memory(Cookie.session("myappid")));
get("/", ctx -> {
Session session = ctx.session(); // 1
session.put("foo", "bar"); // 2
return session.get("foo").value(); // 3
});
}
-
Finds an existing session or creates a new one.
-
Sets a session attribute.
-
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:
{
setSessionStore(SessionStore.memory(new Cookie("SESSION"))); // 1
get("/", ctx -> {
Session session = ctx.session();
session.put("foo", "bar");
return session.get("foo").value();
});
}
-
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:
{
setSessionStore(SessionStore.memory(SessionToken.header("TOKEN"))); // 1
get("/", ctx -> {
Session session = ctx.session();
session.put("foo", "bar");
return session.get("foo").value();
});
}
-
The Session Token/ID is read from the
TOKENHTTP header.
You can also combine both methods, telling Jooby to check the cookie first, and then fall back to the header:
{
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();
});
}
-
The Session Token/ID is read from the
SESSIONcookie or theTOKENheader (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
HmacSHA256to 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).
{
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();
});
}
-
A secure, 32-byte secret key is required to sign the data.
-
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:
{
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:
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.
{
sse("/sse", sse -> { // 1
sse.send("Welcome"); // 2
});
}
-
Connection established.
-
Send a message to the client.
Message Options
Additional message properties (like custom events, IDs, and retry timeouts) are available via the ServerSentMessage class:
{
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.
{
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:
{
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:
{
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
});
});
}
-
Register a WebSocket handler.
-
On connection (open), send a message back to the client. This is also a good place to initialize resources.
-
On receiving a new message, send a response back to the client.
-
The WebSocket is about to close. You must free/release any acquired resources here.
-
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.
{
ws("/ws/{key}", (ctx, configurer) -> {
String key = ctx.path("key").value(); // 1
String foo = ctx.session().get("foo").value(); // 2
// ...
});
}
-
Access a path variable (
key). -
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.
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
});
});
}
-
Install the Jackson module (required for JSON decoding/encoding).
-
Parse and decode the incoming message to a
MyObject. -
Encode
myobjectas 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:
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:
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:
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:
-
Services: The objects and dependencies your application needs to run.
-
Extensions: The mechanism for packaging and registering those services, along with routes and configuration.
-
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:
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.
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.
Checkout our dependency injection modules.
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.
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
}
}
-
Create the service.
-
Save the service into the application’s service registry.
-
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:
public class App extends Jooby {
{
install(new MyExtension()); // 1
get("/", ctx -> {
DataSource ds = require(DataSource.class); // 2
// Use the datasource...
return "Success";
});
}
}
-
Install the custom extension.
-
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
-
AWS-SDK v2: Amazon Web Service module SDK 2.
-
AWS SDK v1: Amazon Web Service module SDK 1.
Data
-
Ebean: Ebean ORM module.
-
Flyway: Flyway migration module.
-
GraphQL: GraphQL Java module.
-
HikariCP: A high-performance JDBC connection pool.
-
Hibernate: Hibernate ORM module.
-
Jdbi: Jdbi module.
-
Kafka: Kafka module.
-
Redis: Redis module.
-
Vertx mySQL client: Vertx reactive mySQL client module.
-
Vertx Postgres client: Vertx reactive Postgres client module.
Dependency Injection
-
Avaje Inject: Avaje Inject module.
-
Guice: Guice module.
Validation
-
Avaje Validator: Avaje Validator module.
-
Hibernate Validator: Hibernate Validator module.
JSON
-
Avaje-JsonB: Avaje-JsonB module for Jooby.
-
Gson: Gson module for Jooby.
-
Jackson2: Jackson2 module for Jooby.
-
Jackson3: Jackson3 module for Jooby.
-
JSON-B: JSON-B module for Jooby.
Template Engine
-
Handlebars: Handlebars template engine.
-
JStachio: JStachio template engine.
-
jte: jte template engine.
-
Freemarker: Freemarker template engine.
-
Pebble: Pebble template engine.
-
Rocker: Rocker template engine.
-
Thymeleaf: Thymeleaf template engine.
Scheduler
-
DbScheduler: Db scheduler module.
-
Quartz: Quartz scheduler module.
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.
{
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’srunAppmethods). -
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:
-
${user.dir}/conf/application.conf(External file system) -
${user.dir}/application.conf(External file system) -
classpath://application.conf(Internal jar resource)
|
Note
|
|
Overrides
Properties are resolved using the following precedence (highest priority first):
-
Program arguments (e.g.,
java -jar app.jar foo=bar) -
System properties (e.g.,
-Dfoo=bar) -
Environment variables (e.g.,
foo=bar java -jar app.jar) -
Environment-specific property file (e.g.,
application.prod.conf) -
Default property file (
application.conf)
{
Environment env = getEnvironment(); // 1
Config conf = env.getConfig(); // 2
System.out.println(conf.getString("foo")); // 3
}
-
Retrieve the current environment.
-
Access the underlying
Configobject. -
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.
└── 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.
{
setEnvironmentOptions(new EnvironmentOptions().setFilename("myapp.conf")); // 1
}
-
Loads
myapp.confinstead of the defaultapplication.confwhile maintaining standard precedence rules.
{
Config conf = ConfigFactory.load("custom.conf"); // 1
Environment env = new Environment(getClassLoader(), conf); // 2
setEnvironment(env); // 3
}
-
Manually load a configuration file.
-
Wrap it in a Jooby Environment.
-
Assign it to the application before startup.
Logging
Jooby uses SLF4J, allowing you to plug in your preferred logging framework.
Logback
-
Add Dependency:
logback-classic. -
Configure: Place
logback.xmlin yourconfdirectory or classpath root.
Log4j2
-
Add Dependencies:
log4j-slf4j-implandlog4j-core. -
Configure: Place
log4j2.xmlin yourconfdirectory 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 |
Application Properties
| Property | Default | Description |
|---|---|---|
|
|
Charset for encoding/decoding and templates. |
|
|
Active environment names. Jooby optimizes performance for non- |
|
|
Supported languages for |
|
|
Temporary directory for the application. |
|
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:
<plugins>
<plugin>
<groupId>io.jooby</groupId>
<artifactId>jooby-maven-plugin</artifactId>
<version>4.0.16</version>
</plugin>
</plugins>
2) Configure the Main Class:
<properties>
<application.class>myapp.App</application.class>
</properties>
3) Launch the Application:
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 |
Options
Below are the available configuration options with their default values:
<configuration>
<mainClass>${application.class}</mainClass>
<restartExtensions>conf,properties,class</restartExtensions>
<compileExtensions>java,kt</compileExtensions>
<port>8080</port>
<waitTimeBeforeRestart>500</waitTimeBeforeRestart>
<useSingleClassLoader>false</useSingleClassLoader>
</configuration>
-
The application’s entry point (Main class).
-
Extensions that trigger an immediate restart.
-
Extensions that trigger a compilation followed by a restart.
-
The local development port.
-
The delay (in milliseconds) to wait after the last file change before restarting. Default is
500ms. -
If
true, Jooby uses a single "fat" classloader. Set this totrueif 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:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-test</artifactId>
<version>4.0.16</version>
</dependency>
2) Define your Application:
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.
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:
@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.
@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.
-
Add the Dependency:
-
Write the Test: Annotate your test class with
@JoobyTest.
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());
}
}
}
-
Jooby starts the application before the test and stops it afterward.
-
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:
@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 |
Using a Factory Method
If your application requires constructor arguments, you can specify a factoryMethod to instantiate it:
@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:
<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:
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.
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.
{
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). Use0for a random port. -
server.ioThreads: Number of IO threads (Netty/Undertow). Defaults toProcessors * 2. -
server.workerThreads: Number of worker threads. Defaults toioThreads * 8. -
server.maxRequestSize: Maximum request size in bytes. Exceeding this triggers a413 Request Entity Too Largeresponse. -
server.defaultHeaders: Automatically setsDate,Content-Type, andServerheaders. -
server.expectContinue: Enables support for100-Continuerequests.
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:
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.
server.ssl.type = X509
server.ssl.cert = "path/to/server.crt"
server.ssl.key = "path/to/server.key"
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.
{
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.
{
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:
<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. |
<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:
-
Create the configuration: Define a
src/etc/stork/stork.ymlfile.
# 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
-
Add the Maven Tiles plugin: Use the Jooby Stork tile to automate the build.
<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>
-
Build the package: Run
mvn package. The resulting Stork.zipfile will be located in thetargetdirectory.
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:
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 |
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 |
io.jooby.Router.setTrustProxy |
RouterOptions.setTrustProxy |
Moved to |
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:
<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) |
|
SslOptions.setCert(String) |
SslOptions.setCert(InputStream) |
Replaced the string argument with |
SslOptions.setTrustCert(String) |
SslOptions.setTrustCert(InputStream) |
Replaced the string argument with |
SslOptions.setPrivateKey(String) |
SslOptions.setPrivateKey(InputStream) |
Replaced the string argument with |
FileUpload.destroy |
FileUpload.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:
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:
{
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.
{
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.