Avaje HTTP Server & Client

Library that generates extremely fast adapter code for Javalin and Helidon SE/Nima APIs via Annotation Processing.

Discord Source API Docs Issues Releases
Discord Github Javadoc Github

 

This library enables your service to be fast and light at runtime by using source code generation (java annotation processors) to adapt annotated rest controllers with (@Path, @Get, @Post etc) to Javalin, Helidon SE and similar web routing http servers.

Effectively we are replacing Jersey or RestEasy with source code generation and the capabilities of Javalin or Helidon SE (web routing).

The generated source code is very simple and readable, so developers can navigate to it and add break points and debug just as if we wrote it all manually ourselves.

What we lose in doing this is automatic Content negotiation. For example, if we need endpoints that serve response content as either JSON or XML content based on request headers then we would need to handle this ourselves.

Summary

JAX-RS Annotations

As we are using Java annotation processing our generators are exposed to more information than is obtained via reflection at runtime. This means we can reduce annotation verbosity making nicer cleaner API's.

A design decision has been to not use JAX-RS annotations. This is because the JAX-RS annotations are a lot more verbose than we desire and because they are not provided as a nice clean separate dependency. The JAX-RS API has a lot of extra weight that we do not need.

HTTP Client

Avaje http client is a lightweight wrapper of JDK HttpClient that also supports Client API with annotation processing to generate source code that implements the API.

Quick Start

1. Add avaje-http-api dependency.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-http-api</artifactId>
  <version>${avaje-http.version}</version>
</dependency>

2. Add the generator module for your desired microframework as a annotation processor.

<!-- Annotation processors -->
<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-http-{helidon/javalin}-generator</artifactId>
  <version>${avaje-http.version}</version>
  <scope>provided</scope>
  <optional>true</optional>
</dependency>

2a. JDK 23+

In JDK 23+, annotation processors are disabled by default, so we need to add a compiler property to re-enable.

<properties>
  <maven.compiler.proc>full</maven.compiler.proc>
</properties>

3. Define a Controller (These APT processors work with both Java and Kotlin.)

package org.example.hello;

import io.avaje.http.api.Controller;
import io.avaje.http.api.Get;
import io.avaje.http.api.Path;
import java.util.List;

@Path("/widgets")
@Controller
public class WidgetController {
  private final HelloComponent hello;
  public WidgetController(HelloComponent hello) {
    this.hello = hello;
  }

  @Get("/{id}")
  Widget getById(int id) {
    return new Widget(id, "you got it"+ hello.hello());
  }

  @Get()
  List<Widget> getAll() {
    return List.of(new Widget(1, "Rob"), new Widget(2, "Fi"));
  }

  record Widget(int id, String name){};
}

Java Module Setup

In the module-info.java we need to define the avaje modules:

Example module-info
module org.example {

  requires io.avaje.http.api;
  // if using javalin specific actions like @Before/@After
  //requires static io.avaje.http.api.javalin;

}

Generated Adapter

Given the above controller and the corresponding framework generator, the below class will be generated

Helidon 4.x
@Generated("avaje-helidon-generator")
@Singleton
public class WidgetController$Route implements HttpFeature {

  private final WidgetController controller;

  public WidgetController$Route(WidgetController controller) {
    this.controller = controller;
  }

  @Override
  public void setup(HttpRouting.Builder routing) {
    routing.get("/widgets/{id}", this::_getById);
    routing.get("/widgets", this::_getAll);
  }

  private void _getById(ServerRequest req, ServerResponse res) throws Exception {
    res.status(OK_200);
    var pathParams = req.path().pathParameters();
    var id = asInt(pathParams.first("id").get());
    var result = controller.getById(id);
    res.send(result);
  }

  private void _getAll(ServerRequest req, ServerResponse res) throws Exception {
    res.status(OK_200);
    var result = controller.getAll();
    res.send(result);
  }
}
Javalin
@Generated("avaje-javalin-generator")
@Singleton
public class WidgetController$Route implements Plugin {

  private final WidgetController controller;

  public WidgetController$Route(WidgetController controller) {
    this.controller = controller;
  }

  @Override
  public void apply(Javalin app) {

    app.get("/widgets/{id}", ctx -> {
      ctx.status(200);
      var id = asInt(ctx.pathParam("id"));
      var result = controller.getById(id);
      ctx.json(result);
    });

    app.get("/widgets", ctx -> {
      ctx.status(200);
      var result = controller.getAll();
      ctx.json(result);
    });
  }
}
Helidon 4.x (Avaje-Jsonb on classpath)
@Generated("avaje-helidon-generator")
@Singleton
public class WidgetController$Route implements HttpFeature {

  private final WidgetController controller;
  private final JsonType<WidgetController.Widget> widgetController$WidgetJsonType;
  private final JsonType<List<WidgetController.Widget>> listWidgetController$WidgetJsonType;

  public WidgetController$Route(WidgetController controller, Jsonb jsonb) {
    this.controller = controller;
    this.widgetController$WidgetJsonType = jsonb.type(WidgetController.Widget.class);
    this.listWidgetController$WidgetJsonType = jsonb.type(WidgetController.Widget.class).list();
  }

  @Override
  public void setup(HttpRouting.Builder routing) {
    routing.get("/widgets/{id}", this::_getById);
    routing.get("/widgets", this::_getAll);
  }

  private void _getById(ServerRequest req, ServerResponse res) throws Exception {
    res.status(OK_200);
    var pathParams = req.path().pathParameters();
    var id = asInt(pathParams.first("id").get());
    var result = controller.getById(id);
    res.headers().contentType(MediaTypes.APPLICATION_JSON);
    //jsonb has a special accommodation for helidon to improve performance
    widgetController$WidgetJsonType.toJson(result, JsonOutput.of(res));
  }

  private void _getAll(ServerRequest req, ServerResponse res) throws Exception {
    res.status(OK_200);
    var result = controller.getAll();
    res.headers().contentType(MediaTypes.APPLICATION_JSON);
    listWidgetController$WidgetJsonType.toJson(result, JsonOutput.of(res));
  }
}
Javalin (Avaje-Jsonb on classpath)
@Generated("avaje-javalin-generator")
@Singleton
public class WidgetController$Route implements Plugin {

  private final WidgetController controller;
  private final JsonType<List<Widget>> listWidgetJsonType;
  private final JsonType<Widget> widgetJsonType;

  public WidgetController$Route(WidgetController controller, Jsonb jsonB) {
    this.controller = controller;
    this.listWidgetJsonType = jsonB.type(Widget.class).list();
    this.widgetJsonType = jsonB.type(Widget.class);
  }

  @Override
  public void apply(Javalin app) {

    app.get("/widgets/{id}", ctx -> {
      ctx.status(200);
      var id = asInt(ctx.pathParam("id"));
      var result = controller.getById(id);
      widgetJsonType.toJson(result, ctx.contentType("application/json").outputStream());
    });

    app.get("/widgets", ctx -> {
      ctx.status(200);
      var result = controller.getAll();
      listWidgetJsonType.toJson(result, ctx.contentType("application/json").outputStream());
    });
  }
}

Usage

The natural way to use the generated adapters is to get a DI library to find and wire them.

Note that there isn't a requirement to use Avaje for dependency injection. Any DI library that can find and wire the generated @Singleton beans can be used.

Usage with Javalin

The annotation processor will generate controller classes implementing the javalin Plugin interface, which means we can register them using:

List<Plugin> routes = ...; //retrieve using a DI framework

Javalin.create(cfg -> routes.forEach(cfg.plugins::register)).start();

Usage with Helidon SE (4.x)

The annotation processor will generate controller classes implementing the Helidon HttpFeature interface, which we can register with the Helidon HttpRouting.

List<HttpFeature> routes = ... //retrieve using a DI framework
final var builder = HttpRouting.builder();

routes.forEach(builder::addFeature);

WebServer.builder()
         .addRouting(builder)
         .build()
         .start();

Dependencies

Maven

See the quick start example

Gradle

See the example at: examples/javalin-gradle-java-basic/build.gradle

Use Gradle version 5.2 or greater which has better support for annotation processing.

Also review the IntelliJ IDEA Gradle settings - see below.

Optional: OpenAPI plugin

Optionally add the io.avaje.openapi plugin to have the openapi.json (swagger) to be generated into src/main/resources/public.

plugins {
  ...
  id('io.avaje.openapi') version('1.2')
}

Dependencies

Add avaje-inject and avaje-http-api as compile dependencies.
Add avaje-http-javalin-generator and javalin-generator as annotation processors.

dependencies {
  ...
  compile('io.avaje:avaje-inject:8.10')
  compile('io.avaje:avaje-http-api:1.20')

  annotationProcessor('io.avaje:avaje-inject-generator:8.10')
  annotationProcessor('io.avaje:avaje-http-javalin-generator:1.20')
}

Kotlin KAPT

For use with Kotlin we change the annotationProcessor to be kapt for the Kotlin compiler.

dependencies {
  ...
  kapt('io.avaje:avaje-inject-generator:8.10')
  kapt('io.avaje:avaje-http-javalin-generator:1.20')
}

OpenAPI Plugin configuration

We can change the location of the generated openapi file by adding an openapi configuration section in build.gradle.

openapi {
  destination = 'other/my-api.json'
}

IntelliJ IDEA with Gradle

We want to delegate the build to Gradle (to properly include the annotation processing) so check our IDEA settings.

Settings / Build / Compiler / Annotation processors

Ensure that Enable annotation processing is disabled so that the build is delegated to Gradle (including the annotation processing):

Settings / Build / Build tools / Gradle

Make sure Build and run is delegated to Gradle.

Optionally set Run tests using to Gradle but leaving it to IntelliJ IDEA should be ok.

Controllers

@Controller
@Path("/contacts")
class ContactController {

  @Get("/{id}")
  Contact getById(long id) {
    ...
  }

  @Post
  void save(Contact contact) {
    ...
  }

  @Delete("/{id}")
  void deleteById(long id) {
    ...
  }
  ...
}

@Controller

Create controllers @Controller. You can provide a path segment that is prepended to any path segments defined by on methods using @Get, @Post, @Put etc. There are three ways to prepend a path.

1. Directly put the path in the controller annotation.

@Controller("/customers")
class CustomerController {
  ...
}

2. Use @Path and @Controller

@Controller
@Path("/customers")
class CustomerController {
  ...
}

3. Use @Path on an Interface and @Controller on an implementing class

@Path("/customers")
interface CustomerController {
  ...
}
@Controller
class CustomerControllerImpl implements CustomerController {
  ...
}

Web Methods on a controller are annotated with HTTP annotations like @Get, @Post, @Put, @Delete.

@Controller("/contacts")
class ContactController {

  private final ContactService contactService;

  @Inject
  ContactController(ContactService contactService) {
    this.contactService = contactService;
  }

  @Get("/{id}")
  Contact getById(long id) {
    ...
  }

  @Get("/find/{type}")
  List<Contact> findByType(String type, @QueryParam String lastName) {
    ...
  }

  @Post
  void save(Contact contact) {
    ...
  }

  ...
}

The controllers can have dependencies injected. The ContactController above can easily have the ContactService dependency injected by avaje-inject.

Controllers are singleton scoped by default

By default controllers are singleton scoped. If the controllers have a dependency on Javalin context, Helidon ServerRequest or ServerResponse then they automatically become request scoped.

@Path

@Path is put on the controller class. The path is prepended to the paths specified by @Get, @Post etc.

@Path("/") is used for the root context path.

Example

The URI's for the RootController below would be:

GET /
GET /foo
@Controller
@Path("/")
class RootController {

  @Get
  @Produces(MediaType.TEXT_PLAIN)
  String hello() {
    return "Hello world";
  }

  @Get("foo")
  @Produces(MediaType.TEXT_PLAIN)
  String helloFoo() {
    return "Hello Foo";
  }

}

The URI's for the CustomerController below are:

GET /customer
GET /customer/active
GET /customer/active/{customerType}
@Controller
@Path("/customer")
class CustomerController {

  @Get
  List<Customer> findAll() {
    ...
  }

  @Get("/active")
  List<Customer> findActive() {
    ...
  }

  @Get("/active/{customerType}")
  List<Customer> findByType(String customerType) {
    ...
  }
}

Module/Package Wide Root Paths

When a @Path annotation is placed on a module-info or package-info file, that path wil be prepended to all controllers contained within the packages and sub-packages.

@Path("/module")
module example.module {
//contents...
}

The URI's for the CustomerController below are:

GET /module/customer
@Controller("/customer")
class CustomerController {

  @Get
  List<Customer> findAll() {
    ...
  }
}

Path parameters

Path parameters start with { and end with }.

For example {id}, {name}, {startDate}.

The path parameter names need to be matched by method parameter names on the controller. For example:

@Get("/{id}/{startDate}/{type}")
List<Bazz> findBazz(long id, LocalDate startDate, String type) {

  // id, startDate, type all match method parameter names
  ...
}

This means that unlike JAX-RS we do not need a @PathParam annotation and this makes our code less verbose and nicer to read.

Note that the JAX-RS equivalent to the above is below. The method declaration starts to get long and harder to read quite quickly.

// JAX-RS "annotation noise" with @PathParam

@GET
@Path("/{id}/{startDate}/{sort}")
List<Bazz> findBazz(@PathParam("id") long id, @PathParam("startDate") LocalDate startDate, @PathParam("sort") String sort) {

  // we start getting "annotation noise" ...
  // making the code hard to read

}

Matrix parameters

Matrix parameters are optional sub-parameters that relate to a specific segment of the path. They are effectively an alternative to using query parameters where we have optional parameters that relate to a specific path segment.

// 'type' path segment has matrix parameters 'category' and 'vendor'

@Get("/products/{type;category;vendor}/available")
List<Product> products(String type, String category, String vendor) {
  ...
}
// example URI's

GET /products/chair/available
GET /products/chair;category=kitchen/available
GET /products/chair;category=kitchen;vendor=jfk/available
// 'type' has matrix parameters 'category' and 'vendor'
// 'range' has matrix parameter 'style'

@Get("/products/{type;category;vendor}/{range;style}")
List<Product> products(String type, String category, String vendor, String range, String style) {
  ...
}
// example URI's

GET /products/chair/commercial
GET /products/chair;category=kitchen/domestic
GET /products/chair;category=kitchen/commercial;style=contemporary
GET /products/chair/commercial;style=classical

JAX-RS @MatrixParam

Our matrix parameters are equivalent to JAX-RS except that they relate by convention to method parameters of the same name and we do not need explicit @MatrixParam.

The JAX-RS equivalent to the above is below. The method declaration starts to get long and harder to read quite quickly.

// JAX-RS "annotation noise" with @MatrixParam and @PathParam

@GET
@Path("/products/{type;category;vendor}/{range;style}")
List<Product> products(@PathParam("type") String type, @MatrixParam("category") String category, @MatrixParam("vendor") String vendor, @PathParam("type") String range, @MatrixParam("style") String style) {

  // we start getting "annotation noise" ...
  // making the code hard to read
  ...
}

@QueryParam

We explicitly specify query parameters using @QueryParam.

// Explicit query parameter order-by

@Get("/{bornAfter}")
List<Cat> findCats(LocalDate bornAfter, @QueryParam("order-by") String orderBy) {
  ...
}

Implied query parameters

Query parameters can be implied by not being a path parameter. That is, if a method parameter does not match a path parameter then it is implied to be a query parameter.

The following 3 declarations are exactly the same with all 3 having a query parameters for orderBy

@Get("/{bornAfter}")
List<Cat> findCats(LocalDate bornAfter, @QueryParam("orderBy") String orderBy) {
  ...
}

@Get("/{bornAfter}")
List<Cat> findCats(LocalDate bornAfter, @QueryParam String orderBy) {
  ...
}

@Get("/{bornAfter}")
List<Cat> findCats(LocalDate bornAfter, String orderBy) {  // orderBy implied as query parameter
  ...
}

Note that we must explicitly use @QueryParam when the query parameter is not a valid java identifier. For example, if the query parameter includes a hyphen then we must use @QueryParam explicitly.

Example

We must use an explicit @QueryParam when the parameter name includes a hyphen like order-by.

// order-by is not a valid java identifier
// ... so we must use explicit @QueryParam here

@Get
List<Cat> findCats(@QueryParam("order-by") String orderBy) {
  ...
}

Query parameter types

Query parameters can be one of the following types: String, Integer, Long, Short, Float, Double, Boolean, BigDecimal, UUID, LocalDate, LocalTime, LocalDateTime, or Enums(Will use Enum.valueOf(EnumType, parameter) ). To get multivalue parameters we can use List<T> or Set<T> where T is any of the previously mentioned types. To get all query parameters define a parameter of type Map<List<T>>.

Query parameters are considered optional / nullable.

@BeanParam

We can create a bean and annotate it in a controller method with @BeanParam. The properties on the bean default to being query parameters.

We typically do this when we have a set of query parameters/headers that are common / shared across a number of endpoints.

public class CommonParams {

  private Long firstRow;
  private Long maxRows;
  private String sortBy;
  private Set<String> filter;
  //you can use ignore to mark a field as not a request parameter
  @Ignore
  private String ignored;

  //getters/setters or a constructor
}

We annotate the bean with @BeanParam

@Get("search/{type}")
List<Cat> findCats(String type, @BeanParam CommonParams params) {

  ...
}

The generated Javalin code for the above is:

ApiBuilder.get("/cats/search/{type}", ctx -> {
  ctx.status(200);
  String type = ctx.pathParam("type");
  CommonParams params = new CommonParams();
  params.setFirstRow(toLong(ctx.queryParam("firstRow")));
  params.setMaxRows(toLong(ctx.queryParam("maxRows")));
  params.setSortBy(ctx.queryParam("sortBy"));
  params.setfilter(list(Objects::toString, ctx.queryParams("filter")));

  ctx.json(controller.findCats(type, params));
});

@Form

@BeanParam and @Form are very similar except with @Form beans the properties default to form parameters instead of query parameters.

JAX-RS @BeanParam

Our @BeanParam is virtually the same as JAX-RS @BeanParam except the properties default to being query parameters, whereas with JAX-RS we need to annotate each of the properties. We can do this because we have @Form and "Form beans".

BeanParam beans with @Header, @Cookie properties

The properties on a "bean" default to being query parameters. We put @Header or @Cookie on properties that are instead headers or cookies.

public class CommonParams {

  private Long firstRow;
  private Long maxRows;
  private String sortBy;
  private String filter

  @Header
  private String ifModifiedSince;

  @Cookie
  private String myState;

  //getters/setters or a constructor
}

Request Body

Avaje auto detects that a parameter is a request body if the type is a POJO/byte[]/InputStream and not marked with a @BeanParam annotation. To mark a string parameter as a body, use the @BodyString annotation.

@Post
void save(Customer customer) {
  ...
}

Generated for Javalin

The code generated code for Javalin for save() above is:

ApiBuilder.post("/customers", ctx -> {
  ctx.status(201);
  Customer customer = ctx.bodyStreamAsClass(Customer.class);
  controller.save(customer);
});

@Form

If a method has both @Post and @Form then the method parameters default to be form parameters.

In the following example name, email and url all default to be form parameters.

@Form @Post("register")
void register(String name, String email, String url) {
  ...
}

@FormParam

For the example above we could alternatively use explicit @FormParam on each of the form parameters rather than @Form. We then get:

@Post("register")
void register(@FormParam String name, @FormParam String email, @FormParam String url) {
  ...
}

The expectation is that we most often would use @Form because it reduces "annotation noise" and that we will very rarely use @FormParam. Potentially we only use @FormParam if the parameter name includes hyphen or similar characters that are not valid Java/Kotlin identifiers.

Generated for Javalin

The generated Javalin code for both cases above is the same:

ApiBuilder.post("/customers/register", ctx -> {
  ctx.status(201);
  String name = ctx.formParam("name");
  String email = ctx.formParam("email");
  String url = ctx.formParam("url");
  controller.register(name, email, url);
});

@Default

We can use @Default to specify a default value for form parameters.

@Form @Post("register")
void register(String name, String email, @Default("http://localhost") String url) {
  ...
}

@Form "Form Beans"

In the case where we are posting a form with a lot of parameters we can define a bean with properties for each of the form parameters rather than have a method with lots of arguments.

"Form beans" can have a constructor with arguments. They do not require a no-arg constructor.

Using a form bean can make the code nicer and gives us a nicer option to use validation annotations on the "form bean" properties.

public class MyForm {

  @Size(min=2, max=100)
  private String name;
  private String email;
  //getters/setters/constructors
}
@Form
@Post("register")
void register(MyForm myForm) {
  ...
}
The generated Javalin code for the above is.
ApiBuilder.post("/contacts/register", ctx -> {
  ctx.status(201);
  MyForm myForm = new MyForm(ctx.formParam("name"), ctx.formParam("name"), ctx.formParam("email"));
  controller.register(myForm);
});

"Form beans" are nice with forms with lots of properties because they de-clutter our code and the generated code takes care of putting the values into our bean properties so that we don't have to write that code.

This use of @Form is very similar to JAX-RS @BeanParam except that the bean properties default be being form parameters. With JAX-RS we would put a @FormParam on every property that is a form parameter which becomes a lot of annotation noise on a large form.

Kotlin data class

Kotlin data classes are a natural fit for form beans.

data class SaveForm(var id: Long, var name: String, var someDate: LocalDate?)


@Form @Post
fun saveIt(form: SaveForm) {

  ...
}

The generated code for the above controller method is:

ApiBuilder.post("/", ctx -> {
  ctx.status(201);
  SaveForm form =  new SaveForm(
    asLong(checkNull(ctx.formParam("id"), "id")),     // non-nullable type
    checkNull(ctx.formParam("name"), "name"),         // non-nullable type
    toLocalDate(ctx.formParam("someDate"))
  );

  controller.saveIt(form);
});

If the form bean has Kotlin non-nullable types (id and name above) then the generated code includes a null check when populating the bean (the checkNull() method).

If there is not a value for a non-nullable Kotlin property then a validation error will be thrown at that point (this validation exception is thrown relatively early compared to using bean validation on Java form beans).

Form beans with @QueryParam, @Header, @Cookie properties

The properties on a "form bean" default to being form parameters. We put @QueryParam, @Header or @Cookie on properties that are instead query params, headers or cookies.

public class MyForm {

  @Size(min=2, max=100)
  public String name;
  public String email;
  public String url;

  @QueryParam
  public Boolean overrideFlag;

  @Header
  public String ifModifiedSince;

  @Cookie
  public String myState;
}

The generated code populates from query params, headers and cookies. The generated code is:

ApiBuilder.post("/contacts/register", ctx -> {
  ctx.status(201);
  MyForm myForm =  new MyForm();
  myForm.name = ctx.formParam("name");
  myForm.email = ctx.formParam("email");
  myForm.url = ctx.formParam("url");
  myForm.overrideFlag = toBoolean(ctx.queryParam("overrideFlag"));     // queryParam !!
  myForm.ifModifiedSince = ctx.header("If-Modified-Since");            // header !!
  myForm.myState = ctx.cookie("myState");                              // cookie !!

  controller.register(myForm);
});

@Produces

Use @Produces to modify the response content type and generated OpenAPI definition. When not specified, we default to application/json. We can set the default http status code for the method as well. If not specified, the default status codes for the different http verbs are as follows:
GET(200)
POST(201)
PUT(200, void methods 204)
PATCH(200, void methods 204)
DELETE(200, void methods 204)

@Path("/")
@Controller
class RootController {

  private Service service;

  //send plain text
  @Get
  @Produces(MediaType.TEXT_PLAIN)
  String hello() {
    return "Hello world";
  }

  // default json
  @Get("obj")
  Example helloObj() {
    return new Example();
  }

  // we can also send our data as a byte array
  @Get("png")
  @Produces(MediaType.IMAGE_PNG)
  byte[] helloByte() {
    return service.getPNG();
  }

  // use Javalin Context for our response
  // in this case Produces is only needed for the OpenAPI generation
  @Get("ctx")
  @Produces(MediaType.IMAGE_PNG)
  void helloCTX(Context ctx) {
    service.writeResponseDirectly(ctx.outputStream());
  }

}

Use @Header for a header parameter. It the header parameter name is not explicitly specified then it is the init caps snake case of the parameter name.

userAgent -> User-Agent

lastModified -> Last-Modified

@Post
Bar postIt(Foo payload, @Header("User-Agent") String userAgent) { // explicit
  ...
}

@Post
Bar postIt(Foo payload, @Header String userAgent) { // User-Agent
  ...
}

@Get
Bazz find(@Header String lastModified) { // Last-Modified
  ...
}

Use @Cookie for a Cookie parameter.

@Post("bar/{name}")
Bar bar(String name, @Cookie("my-cookie") String myCookie) {
  ...
}

@Post("foo/{name}")
Foo foo(String name, @Cookie String myCookie) {
  ...
}

The generated Helidon code for the method above is:

private void _foo(ServerRequest req, ServerResponse res) {
  String name = req.path().param("name");
  String myCookie = req.headers().cookies().first("myCookie").orElse(null);
  res.send(controller.fooMe(name, myCookie));
}

@Default

We can use @Default to specify a default value for a Query Parameter/Header/Cookie/Form Parameter.

@Get("/catty")
List<Cat> findCats(@Header @Default("age") String orderBy, @Default({"1", "2"}) List<Integer> numbersOfLimbs) {
  ...
}

@Filter

Annotate methods with @Filter for HTTP filter web routes. Filter web routes behave similarly to void @Get methods (They can use header/query/cookie parameters with type conversion)

Helidon

Helidon filters must have a FilterChain parameter, and optionally can add RoutingRequest and RoutingResponse.

@Filter
void filter(FilterChain chain, RoutingRequest req, RoutingResponse res) {
 //... filter logic
}

Javalin

Javalin filters correspond to before handlers, and we can add a Context parameter.

@Filter
void filter(Context ctx) {
 //... filter logic
}

@ExceptionHandler

As the name implies, this annotation marks a handler method for handling exceptions that are thrown by other handlers.

Exception handler methods may have parameters of the following types:

  1. An exception argument: declared as a general Exception or as a more specific exception. This also serves as a mapping hint if the annotation itself does not specify the exception types.
  2. Request and/or response objects (typically from the microframework). We can choose any specific request/response type. e.g. Javalin's Context or Helidon's ServerRequest/ServerResponse.

Handler methods may be void or return an object for serialization. When returning an object, we can combine the @ExceptionHandler annotation with @Produces for a specific HTTP error status and media type.

Helidon

@ExceptionHandler
@Produces(statusCode = 501)
Person exceptionCtx(Exception ex, ServerRequest req, ServerResponse res) {
  return new Person();
}

@ExceptionHandler(IllegalStateException.class)
void exceptionVoid(ServerResponse res) {
 //error logic
}

Javalin

@ExceptionHandler
@Produces(statusCode = 501)
Person exceptionCtx(Exception ex, Context ctx) {
  return new Person();
}

@ExceptionHandler(IllegalStateException.class)
void exceptionVoid(Context ctx) {
 //error logic
}

(Javalin-only) @Before/@After

For Javalin applications, we can use @Before/@After to mark a handler as a Javalin before/after handler.

@Before("/path")
void before(Context ctx) {
 //... before logic
}

@After("/path")
void after(Context ctx) {
 //... after logic
}

Context

Javalin Context

The Javalin Context can be passed as a method argument or injected as a dependency of the controller.

Context as method argument

@Get
Response save(HelloDto dto, Context context) {
  // use Javalin context as desired
  ctx.status(202);
  ...
  return new Response();
}

@Post
void save(HelloDto dto, Context context) {
  // use Javalin context as desired
  ...
}

Helidon request/response

Helidon has ServerRequest and ServerResponse and these can be passed as a method argument of injected as a dependency of the controller.

ServerRequest/ServerResponse as method argument

@Get
Response save(HelloDto dto, ServerRequest request, ServerResponse response) {
  // use Helidon server request or response as desired
  ...
  return new Response();
}
@Post
void save(HelloDto dto, ServerRequest request, ServerResponse response) {
  // use Helidon server request or response as desired
  ...
}

Controllers are singleton scoped by default

By default controllers are singleton scoped. When we pass context objects like Javalin Context or Helidon ServerRequest as method arguments then the controllers remain singleton scoped.

Request scoped controllers

We can define the Javalin context, Helidon ServerRequest or ServerResponse as a dependency to be injected using constructor injection or field injection (rather than passed as a method argument).

avaje-inject knows that these types need to be injected per request and automatically makes the controller request scoped.

Request scoped means that a new instance of the controller will be instantiated for each request.

Example

The following ContactController has the Javalin Context as a constructor injected dependency. The controller is request scoped and instantiated per request.

// Automatically becomes request scoped
//  ... because Javalin Context is a dependency
//  ... controller instantiated per request

@Controller("/contacts")
class ContactController {

  private final ContactService contactService;

  private final Context context; // Javalin Context

  // Inject Javalin context via constructor
  @Inject
  ContactController(Context context, ContactService contactService) {
    this.context = context;
    this.contactService = contactService;
  }

  @Get("/{id}")
  Contact getById(long id) {
    // use the javalin context ...
    var fooCookie = context.cookieStore("foo");
    ...
  }
}
Example

In this example ProductController has the Helidon ServerRequest and ServerResponse injected using field injection rather than constructor injection. Note that when using field injection they can not be final and can not be private.

// Automatically becomes request scoped
//  ... because Helidon request and response are a dependency
//  ... controller instantiated per request

@Controller
@Path("/products")
class ProductController {

  private final MyService myService;

  @Inject
  ServerRequest request; // Helidon request field injected

  @Inject
  ServerRequest response; // Helidon response field injected

  @Inject
  ProductController(MyService myService) {
    this.myService = myService;
  }

  @Get("/{id}")
  Contact getById(long id) {
    // use the helidon request ...
    var fooCookie = request.headers().cookies().first("foo");
    ...
  }
}

Instrumenting the Server Context

The @InstrumentServerContext annotation marks a controller method to be instrumented with RequestContextResolver. For the execution of the controller method, the server context will be stored by the given implementation of RequestContextResolver.

By default, a RequestContextResolver implementation using ThreadLocals is provided to store the Context instance. When using virtual threads, it may be better to provide an implementation using Scoped Values.

Using RequestContextResolver

RequestContextResolver resolver = ...

@Get
@InstrumentServerContext
void helloWorld(long id) {
 Context ctx = resolver.currentRequest().orElseThrow().response()
 ctx.result("success");
  ...
}

Type conversions

There are built in type conversions for the following types:

For multivalue parameters like query parameters or headers, we can use List<T> or Set<T> where T is any of the previously mentioned types.

In the following example there is a type conversion for startDate and active.

@Get("/{id}/{name}")
Hello hello(int id,
       String name,
       LocalDate startDate,
       Boolean active,
       List<Long> longs
) {
  ...
}

For example, the Javalin generated code below includes the type conversion with toLocalDate() and toBoolean().

ApiBuilder.get("/hello/{id}/{name}", ctx -> {
  ctx.status(200);
  int id = asInt(ctx.pathParam("id"));
  String name = ctx.pathParam("name");
  LocalDate startDate = toLocalDate(ctx.queryParam("startDate"));
  Boolean active = toBoolean(ctx.queryParam("active"));
  List<Long> longs = list(PathTypeConversion::toLong, ctx.queryParams("longs"));
  ctx.json(controller.hello(id, name, startDate, active, longs));
});

Conversion Exception Handling

If a parameter fails type conversion then InvalidPathArgumentException is thrown. This exception is typically mapped to a 404 response in the exception handler.

Note that path conversions imply the value can NOT be null. All other parameter types are considered optional/nullable.

InvalidTypeArgumentException is throw for non-path parameter conversions that fail such as conversions in form beans, query parameters, headers and cookies.

We should register exception handlers for these 2 exceptions like the handlers below:

record ErrorResponse(String path, String message){};

@Produces(statusCode = 404)
@ExceptionHandler(InvalidPathArgumentException.class)
ErrorResponse validException(Context ctx) {

 return new ErrorResponse(ctx.path(), "invalid path argument");
}

@Produces(statusCode = 400)
@ExceptionHandler(InvalidTypeArgumentException.class)
ErrorResponse validException(Context ctx) {

 return new ErrorResponse(ctx.path(), "invalid type argument");
}

Bean validation

We can optionally add bean validation through the validator interface. We can validate a request body and Form Bean/BeanParam

Example: HelloController

Add @Valid

Add a jakarta/javax/avaje @Valid annotation on controllers/methods and the types that we want bean validation to be included for. When we do this, controller methods that take a request payload will then have the request bean (populated by JSON payload or form/header/query parameters) validated before it is passed to the controller method.

The avaje-http @Valid annotation can additionally be used to set the Validation Groups to use while validating.

For the controller method below:

@Valid
class HelloForm {
  @NotBlank
  private String name;
  private String email;
  //getters/setters/constructors
}

@Valid
class HelloBean {
  @NotBlank
  private String name;

  @Email(groups=EmailCheck.class)
  private String email;
  //getters/setters/constructors
}

@Valid
class BodyClass {
  @NotBlank
  private String somefield;
  //getters/setters/constructors
}
@Valid
@Controller("/baz")
class BazController  {

  @Form
  @Post("/form")
  void saveForm(HelloForm helloForm) {
    ...
  }

  @io.avaje.http.api.Valid(groups={Default.class,EmailCheck.class})
  @Post("/bean")
  void saveBean(@BeanParam HelloBean helloBean) {
    ...
  }

  @Post("/body")
  void saveBody(BodyClass body) {
    ...
  }

The generated code now includes validation of the beans before they are passed to the controller method. The generated code is:

Helidon 4.x
private String language(ServerRequest req) {
  return req.headers().first(HEADER_ACCEPT_LANGUAGE).orElse(null);
}

private void _saveForm(ServerRequest req, ServerResponse res) throws Exception {
  var formParams = req.content().as(Parameters.class);
  var helloForm =  new HelloForm(
      formParams.first("name").orElse(null),
      formParams.first("email").orElse(null)
    );

  validator.validate(helloForm, language(req));
  controller.saveForm(helloForm, res);
  ...
}

private void _saveBean(ServerRequest req, ServerResponse res) throws Exception {
  var helloBean =  new HelloBean(
      req.query().first("name").orElse(null),
      req.query().first("email").orElse(null)
    );

  validator.validate(helloBean, language(req), Default.class, EmailCheck.class);
  controller.saveBean(helloBean, res);
}

private void _saveBody(ServerRequest req, ServerResponse res) throws Exception {
  res.status(CREATED_201);
  var body = bodyClassJsonType.fromJson(req.content().inputStream());
  validator.validate(body, language(req));
  controller.saveBody(body, res);
}
Javalin
ApiBuilder.post("/baz/form", ctx -> {
  ctx.status(201);
  HelloForm helloForm = new HelloForm(
    ctx.formParam("name"),
    ctx.formParam("email")
  );
  var validLanguage = ctx.header("Accept-Language");
  validator.validate(helloForm, validLanguage);
  controller.saveForm(helloForm);
});

ApiBuilder.post("/baz/bean", ctx -> {
  ctx.status(201);
  HelloBean helloBean = new HelloBean(
    ctx.queryParam("name"),
    ctx.queryParam("email")
  );
  var validLanguage = ctx.header("Accept-Language");
  validator.validate(helloBean, validLanguage, Default.class, EmailCheck.class);
  controller.saveBean(helloBean);
});

ApiBuilder.post("/baz/body", ctx -> {
  ctx.status(201);
  var body = ctx.bodyAsClass(BodyClass.class);
  validator.validate(body);       // validation added here !!
  controller.saveBody(helloBean);
});

Custom Validation

For custom validation, you can can implement the avaje validator interface yourself and add custom logic.

import io.avaje.http.api.Validator;

@Singleton
public class BeanValidator implements Validator {

  @Override
  public void validate(Object bean, String acceptLanguage, Class<?>... groups) {
  //do validation
  // if validation fails throw something
  }
}

Using Avaje Validation

Add a dependency on avaje-validator. This will transitively bring in a Validator instance which will be used to validate beans.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-validator</artifactId>
  <version>${validator.version}</version>
</dependency>

Using Hibernate

Add a dependency on avaje-http-hibernate-validator. This will provide a hibernate Validator instance which will be used to validate.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-http-hibernate-validator</artifactId>
  <!-- use 2.9 for javax validation -->
  <version>3.3</version>
</dependency>

ValidationException handler

Add an exception handler for ValidationException like the one below. With bean validation we collect all the validation errors and these are included in the exception as a map keyed by the property path.

exception.getErrors() in the handler below is returning a Map<String, Object>

record ErrorResponse(String message, List<Violation> violations){};

@Produces(statusCode = 400)
@ExceptionHandler
ErrorResponse validException(ValidationException ex) {

 return new ErrorResponse(ex.getMessage(), ex.getErrors());
}

Roles

We can optionally add declarative security role checking for Javalin and Jex.

Example
@Controller
@Path("/customers")
class CustomerController  {

  @Roles({AppRoles.ADMIN, AppRoles.BASIC_USER})
  @Get("/{id}")
  Customer find(int id) {
    ...
  }

Javalin Roles

Example reference test-javalin - HelloController

Step 1: Create an enum that implements io.javalin.security.RouteRole

Create an enum that implements io.javalin.security.RouteRole.

import io.javalin.security.RouteRole;

public enum AppRoles implements RouteRole {
  ANYONE, ADMIN, BASIC_USER, ORG_ADMIN
}

Step 2: Create a Roles / PermittedRoles annotation

Create an annotation that has a short name that ends with Roles or PermittedRoles. The annotation must follow this naming convention to be detected by the annotation processor (code generation).

The annotation must have a value() attribute that specifies the enum role type specified in Step 1.

Example
package org.example.myapp.web;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Specify permitted roles.
 */
@Target(value={METHOD, TYPE})
@Retention(value=RUNTIME)
public @interface Roles {

  /**
   * Specify the permitted roles.
   */
  AppRoles[] value() default {};
}

Step 3: Use the annotation

Add the annotation to any controller or controller methods as desired.

Example
@Roles({AppRoles.ADMIN, AppRoles.BASIC_USER})
@Get("/{id}")
Customer find(int id) { ...

Step 4: Javalin AccessManager

Ensure that Javalin is setup with an AccessManager to implement the role check.

Javalin app = Javalin.create(config -> {
  ...
  config.accessManager((handler, ctx, permittedRoles) -> {
    // implement role permission check
    ...
  });
});

Jex Roles

Example reference test-jex - HelloController

Step 1: Create an enum that implements io.avaje.jex.Role

Create an enum that implements io.avaje.jex.Role.

import io.avaje.jex.Role

public enum AppRoles implements Role {
  ANYONE, ADMIN, BASIC_USER, ORG_ADMIN
}

Step 2: Create a Roles / PermittedRoles annotation

Create an annotation that has a short name that ends with Roles or PermittedRoles. The annotation must follow this naming convention to be detected by the annotation processor (code generation).

The annotation must have a value() attribute that specifies the enum role type specified in Step 1.

Example
package org.example.myapp.web;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Specify permitted roles.
 */
@Target(value={METHOD, TYPE})
@Retention(value=RUNTIME)
public @interface Roles {

  /**
   * Specify the permitted roles.
   */
  AppRoles[] value() default {};
}

Step 3: Use the annotation

Add the annotation to controller methods or the controller as desired.

Example
@Roles({AppRoles.ADMIN, AppRoles.BASIC_USER})
@Get("/{id}")
Customer find(int id) { ...

Step 4: Jex AccessManager

Ensure that Jex is setup with an AccessManager to implement the role check.

Example
jex.accessManager((handler, ctx, permittedRoles) -> {
  ...
})

OpenAPI

An OpenAPI description of the API is always generated.

The annotation processor that generates the web route adapters also generates a OpenAPI (swagger) definition of all the endpoints that the controllers define.

Example Javalin OpenAPI Controller.
Generated OpenAPI Definition.

Javadoc

The annotation processor reads the javadoc (and Kotlin documentation) of the controller methods to generate the openAPI definition. The javadoc summary, description, @param and @return documentation are used to create the OpenAPI Operation definitions.
The processor reads all the request and response types as OpenAPI component schema. The various annotations like @Header,@Param, and @Produces also modify the generated OpenAPI docs.

/**
 * Example of Open API Get (up to the first period is the operation summary).
 *
 * basic Post (This Javadoc description is added to the operation description)
 *
 * @param b the body (the param docs are used for query/header/body description)
 * @return funny phrase (this part of the javadoc is added to the response desc)
 */
@Post("/post")
ResponseModel endpoint(RequestModel model) {
...
}

In Addition, the generator can read the request/response class javadoc to generate openAPI component description.

class ResponseModel {
  /** field one */
  String field1;
  /** field two */
  String field1;
 }

@Consumes

Adding the @Consumes annotation to a controller method let's you control what media type should be generated for the request body in the openAPI definition. This is useful when you are dealing with non-standard request content types.

@Deprecated

Adding Javas's @Deprecated annotation to a controller method marks it as deprecated in the openAPI definition.

@OpenApiResponse

This annotation lets you specify alternate endpoint response status codes/descriptions/types. This is useful for defining error scenarios, and when using the underlying Javalin/Helidon contructs in void methods.

@Post("/post")
@OpenAPIResponse(responseCode = 200, description = "overrides @return javadoc description")
@OpenAPIResponse(responseCode = 201) // Will use @return javadoc description
@OpenAPIResponse(
     responseCode = 404,
     description = "User not found (Will not have an associated response schema)")
@OpenAPIResponse(
     responseCode = 500,
     description = "Some other Error (Will have this error class as the response class reference)",
     type = ErrorResponse.class)
ResponseModel endpoint() {}

Validation Annotations - @NotNull, @Size, @Email etc

The javax bean validation annotations @NotNull, @Size and @Email are read as well as detecting Kotlin non-nullable types as required properties.

These are used to specify required properties, min max lengths and property format.

Swagger annotations

We can add a dependency on io.swagger.core.v3:swagger-annotations:2.0.8 and add swagger annotations.

@OpenAPIDefinition

We use @OpenAPIDefinition to define a title and description for the api. This annotation would often go on the Main method class or on package-info.java in the controllers package.

Example @OpenAPIDefinition

@OpenAPIDefinition(
  info = @Info(
    title = "Example service",
    description = "Example Javalin controllers with Java and Maven"))

@Hidden

Put @Hidden on controller methods that we want to exclude from the OpenAPI documentation.

@Tags

Put @Tags on controller methods to add tags to the OpenAPI Operation documentation.

@SecurityScheme and @SecurityRequirement

Put @SecurityScheme or @SecurityRequirement on controller methods to add to the OpenAPI Operation documentation.

Gradle plugin

The gradle plugin by default puts the generated openapi.json file into src/main/resource/public

To add the plugin:

plugins {
  ...
  id('io.avaje.openapi') version('1.0')
}

To configure the openapi.json file to go to another location add a openapi configuration section in build.gradle like:

openapi {
  destination = 'other/my-api.json'
}

Maven Plugin

The maven plugin by default puts the generated openapi.json file into src/main/resource/public

To add the plugin into build / plugins section:

<plugin>
  <groupId>io.avaje</groupId>
  <artifactId>openapi-maven-plugin</artifactId>
  <version>1.0</version>
  <executions>
    <execution>
      <id>main</id>
      <phase>process-classes</phase>
      <goals>
        <goal>openapi</goal>
      </goals>
    </execution>
  </executions>
</plugin>

To configure the openapi.json file to go to another location add a configuration section element like:

<plugin>
  <groupId>io.avaje</groupId>
  <artifactId>openapi-maven-plugin</artifactId>
  <version>1.0</version>
  <configuration>
    <destination>other/my-api.json</destination>
  </configuration>
  <executions>
    <execution>
      <id>main</id>
      <phase>process-classes</phase>
      <goals>
        <goal>openapi</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Serving openapi.json

When the openapi.json file is in src/main/resources/public it can be served by using addStaticFiles on the JavalinConfig like:

Javalin app = Javalin.create(config -> {
  ...
  config.addStaticFiles("public", Location.CLASSPATH);
});

JAX-RS

Why not use the standard JAX-RS annotations?

 

avaje http

JAX-RS

@Controller -
@Path @Path

HTTP Methods

@Delete @DELETE + @Path
@Get @GET + @Path
@Post @POST + @Path
@Put @PUT + @Path
@Patch @PATCH + @Path

Bean parameters

@Form -
@BeanParam @BeanParam

Parameters

Not needed (implied) @PathParam
Not needed (implied) @MatrixParam
@FormParam + @Default @FormParam + @DefaultValue
@QueryParam + @Default @QueryParam + @DefaultValue
@Header @HeaderParam
@Cookie @CookieParam