Jooby Web Framework Documentation

repository·main·Indexed 23 days ago

https://github.com/jooby-project/jooby

A modular, high-performance web framework for Java and Kotlin. Jooby supports multiple server engines including Netty, Jetty, and Undertow, and offers both a fluent lambda-based Script API and an annotation-based MVC API. It features native Kotlin integration, reactive support for Coroutines, RxJava, and Reactor, and a modular architecture with over 50 thin modules.

Tokens
92.8K
Snippets
302
Records
456
Agent score
83%

What's inside Jooby

  1. Overview of Jooby Features

    main

    Jooby is a modular, high-performance web framework for Java and Kotlin with the following key capabilities:

    • Flexible Routing: Supports both a fluent Script/Lambda API and MVC annotations (via Jooby or JAX-RS).
    • Execution Models: Choose between an Event Loop or worker threads.
    • Reactive Support: Native support for reactive responses using CompletableFuture, RxJava, Reactor, Mutiny, and Kotlin Coroutines.
    • Server Agnostic: Can run on Jetty, Netty, Vert.x, or Undertow.
    • Modern Tooling: Includes OpenAPI 3 support, hot-reload for development, and Model Context Protocol (MCP) for AI/LLM integration.
    • Observability: Native OpenTelemetry support for distributed tracing, metrics, and log correlation.
    • Extensibility: Highly modular architecture via extensions and modules.
  2. What is the Context in Jooby?

    main

    A Context object allows you to interact with the current HTTP request and manipulate the HTTP response. In most cases, you access the Context as a parameter within your route handlers.

    It also provides derived information like matching locales based on the Accept-Language header. To use language matching, you must define the supported languages for your application via application.lang in your configuration or programmatically using setLocales.

    get("/", ctx -> {
      /* do important stuff with the 'ctx' variable */
    });
  3. Manage transactions with UnitOfWork

    main

    The UnitOfWork provides an alternative way to manage the EntityManager and transaction lifecycle. It is useful for manual control within a single block of code.

    • Lifecycle: UnitOfWork automatically begins a transaction. When the apply or accept block returns, the transaction is committed and the EntityManager is closed. If an exception is thrown, the transaction is rolled back.
    • Multiple Transactions: You can use UnitOfWork.TransactionHandler to commit or rollback and immediately start a new transaction within the same block.
    • Limitations: UnitOfWork does not allow nesting and cannot be used together with SessionRequest or TransactionalRequest.
    // Basic usage
    get("/pets", ctx -> require(UnitOfWork.class)
        .apply(em -> em.createQuery("from Pet", Pet.class).getResultList()));
    
    // Multiple transactions within one UnitOfWork
    get("/update", ctx -> require(UnitOfWork.class)
        .apply((em, txh) -> {
          em.createQuery("from Pet", Pet.class).getResultList().forEach(pet -> {
            pet.setName(pet.getName() + " Updated");
            txh.commit(); // commits current and starts a new one
          });
          return "ok";
        }));
  4. Configure SSLHandler for reverse proxies

    main

    When running behind a load balancer or reverse proxy (such as Nginx, HAProxy, or an AWS ALB) that terminates SSL, the SSLHandler relies on the X-Forwarded-Proto header to identify the client's original protocol.

    To ensure the handler correctly identifies the client's original host and protocol, you must enable the setTrustProxy(true) option in your Jooby configuration.

  5. How gRPC routing and fallbacks work in Jooby

    main

    The jooby-grpc module intercepts requests natively before they reach the standard Jooby router.

    • Missing Methods: If a client calls a gRPC method that does not exist, the request falls through to the standard Jooby router, returning a 404 Not Found (which gRPC clients translate to Status 12 UNIMPLEMENTED).
    • Misconfiguration: If you attempt to run gRPC over HTTP/1.1 (instead of HTTP/2), the fallback route will catch the request and throw an IllegalStateException to help you identify the configuration error.
  6. How the MVC API works with annotations

    main

    The MVC API is an annotation-driven alternative to the Script API. Jooby uses an annotation processor to generate source code that defines and executes routes.

    Key Concepts:

    • Generated Classes: For every controller class (e.g., Controller), an annotated class is generated with an underscore suffix (e.g., Controller_).
    • Registration: Jooby does not use classpath scanning. You must explicitly register the generated controller in your application using the mvc() method.
    • Incremental Processing: By default, Jooby uses incremental annotation processing to speed up compilation. This is controlled via the jooby.incremental compiler argument.
    import io.jooby.annotation.*;
    
    @Path("/mvc")
    public class Controller {
    
      @GET
      public String sayHi() {
        return "Hello Mvc!";
      }
    }
    
    public class App extends Jooby {
      {
        // Register the generated controller
        mvc(new Controller_());
      }
    
      public static void main(String[] args) {
        runApp(args, App::new);
      }
    }
  7. Use route attributes for static metadata

    main

    Attributes allow you to attach static metadata to a route during application bootstrap. These are accessible during the request/response cycle via the Context.

    In MVC-style applications, runtime annotations are automatically converted into route attributes. If an annotation has a value() method, the annotation's name becomes the attribute key; otherwise, the method name is used.

    // Setting an attribute manually
    get("/foo", ctx -> "Foo")
      .setAttribute("foo", "bar");
    
    // Accessing attributes in a filter/middleware
    use(next -> ctx -> {
      String role = ctx.getRoute().getAttribute("Role");
      if (user.hasRole(role)) {
        return next.apply(ctx);
      }
      throw new StatusCodeException(StatusCode.FORBIDDEN);
    });
  8. Use Vertx SQL connections with PreparedStatements

    main

    The Vertx SQL modules (e.g., jooby-vertx-mysql-client, jooby-vertx-pg-client) provide high-performance SQL access.

    Core Concept: Thread Safety Each IO thread has an internal Verticle with a dedicated SqlConnection. This connection is only accessible from a Vertx thread. Any attempt to access the connection, PreparedStatement, or PreparedQuery from a non-Vertx thread will result in an exception.

    Usage Pattern:

    1. Define and register prepared statements in the module during installation.
    2. Use ctx.require() to obtain a proxy to the prepared statement/query.
    3. Execute the query within the Vertx event loop.

    Dependency Injection: If using a DI framework, you can inject prepared statements using @Named with the statement name.

    import io.jooby.Reified;
    
    String SELECT_WORLD = "SELECT id, randomnumber from WORLD where id=$1";
    
    // Define the complex type for the prepared query
    Reified<PreparedQuery<RowSet<Row>>> PreparedQueryType = 
        getParameterized(PreparedQuery.class, getParameterized(RowSet.class, Row.class));
    
    {
      install(new VertxPgConnectionModule()
         .preparedStatement(Map.of("selectWorld", List.of(SELECT_WORLD)))      // 1. Create statement
      );
    
      use(vertx());                                                            // 2. Add Vertx handler
    
      var selectWorldQuery = ctx.require(PreparedQueryType, "selectWorld");    // 3. Get proxy
    
      get("/world/{id}", ctx -> {
        return selectWorldQuery.execute(Tuple.of(ctx.path("id").longValue()))
              .map(
                  result -> {
                    var row = result.iterator().next();
                    return new World(row.getInteger(0), row.getInteger(1));
                  });
      });
    }
    
    public static void main(String[] args) {
      runApp(args, new VertxServer(), EVENT_LOOP, App::new);
    }
  9. What are Jooby Modules and how do they work?

    main

    Modules in Jooby are built-in Extensions that bootstrap and configure third-party libraries (such as Jackson, Hibernate, or HikariCP) using Jooby's Extension API.

    Key characteristics:

    • No Abstraction Layers: Unlike many frameworks, Jooby modules do not wrap the underlying library in custom classes. They do not create new layers of abstraction.
    • Direct Access: Modules expose the raw library components directly to your application via the Service Registry. This allows you to use the library's native API exactly as intended by its original creators.
    • Distributed Dependencies: Modules are provided as separate dependencies, allowing you to include only what your application needs.
  10. Use Authorizers to control access

    main

    Authorizers allow you to add custom logic to determine if a user is authorized for a specific path. You can register authorizers in three ways:

    1. Manual configuration: Create a org.pac4j.core.config.Config object, add the authorizer to it, and pass the Config to the Pac4jModule constructor.
    2. Automatic configuration: Pass an instance of the authorizer directly to the .client() method.
    3. Registry/DI integration: Pass the authorizer class to the .client() method, allowing the application registry (Dependency Injection) to provision the instance.
    // Registry/DI integration example
    install(
          new Pac4jModule()
              .client("/api/*", MyTestAuthorizer.class, conf -> {...});
      );
  11. Use TransactionalRequest for automatic lifecycle management

    main

    The TransactionalRequest filter manages the lifecycle of an EntityManager or StatelessSession per HTTP request. It automatically handles creating, binding, beginning, committing, rolling back, and closing the session/transaction. This prevents boilerplate code in your route handlers.

    Note: The transaction does not extend to the rendering phase (JSON, HTML, etc.). Ensure all required data is loaded in the handler to avoid LazyInitializationException during rendering.

    import io.jooby.hikari.HikariModule;
    import io.jooby.hibernate.HibernateModule;
    import io.jooby.hibernate.TransactionalRequest;
    
    {
      install(new HikariModule());
      install(new HibernateModule());
      use(new TransactionalRequest());
    
      post("/create", ctx -> {
        EntityManager em = require(EntityManager.class);
        MyEntity e = ...;
        em.persist(e);
        return e;
      });
    }
  12. Manage collections of services in the registry

    main

    The ServiceRegistry allows you to group multiple services of the same type using Lists, Sets, or Maps. To retrieve these collections, use the Reified type helper to ensure type safety during retrieval.

    • Use listOf(Class) to add to a List.
    • Use mapOf(KeyClass, ValueClass) to add to a Map.
    • Use ctx.require(Reified.list(Class)) to retrieve the collection.
    import io.jooby.Reified;
    import java.util.List;
    
    {
      // 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;
      });
    }