Avaje Jex

Discord Source API Docs Issues Releases
Discord Github Javadoc Github



An uncommonly known fact is that the JDK comes built in with an http server. Avaje Jex is a lightweight (~105kb) wrapper over the built-in api with some key enhancements.

Quick Start

1. Add avaje-jex dependencies.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-jex</artifactId>
  <version>${avaje.jex.version}</version>
</dependency>
 <!-- perhaps also a json dependency like avaje jsonb or jackson  --->

2. Create basic server

Below is an example of a basic server.

Jex.create()
    .get("/", ctx -> ctx.text("hello"))
    .get("/one/{id}", ctx -> ctx.text("one-" + ctx.pathParam("id")))
    .filter(
        (ctx, chain) -> {
          System.out.println("before request");
          chain.proceed();
          System.out.println("after request");
        })
    .error(
        IllegalStateException.class,
        (ctx, exception) -> ctx.status(500).text(exception.getMessage()))
    .port(8080)
    .start();

Handling Requests

With Jex, there are three main handler types: endpoint-handlers, filters, and exception-handlers. Each kind of handler accept a Context instance as one of the parameters and have a void return type. You use methods like ctx.write(result), ctx.json(obj), or ctx.html(html) to set the response which will be returned to the user.

Endpoint Handlers

Endpoint handlers are the main handler type that define your API. You can add a GET handler to serve data to a client, or a POST handler to receive some data. Handlers. Common methods are supported directly on the Routing class (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE)

Endpoint-handlers are matched in the order they are defined.

Jex app = Jex.create();
Routing routing = Jex.create().routing();
routing.get("/output", ctx -> {
    // some code
    ctx.json(object);
});
//alternatively can use a consumer to configure
app.routing(r ->
   r.post("/input", ctx -> {
    // some code
    ctx.status(201);
}));
//or can use convenience methods directly on jex
app.post("/something", ctx -> {
    // some code
    ctx.status(201);
});

Handler paths can include path-parameters. These are available via ctx.pathParam("key"):

var app = Jex.create();
// the {} syntax does not allow slashes ('/') as part of the parameter
app.get("/hello/{name}", ctx -> ctx.write("Hello: " + ctx.pathParam("name")));
// the <> syntax allows slashes ('/') as part of the parameter
app.get("/hello/<name>", ctx -> ctx.write("Hello: " + ctx.pathParam("name")));

Handler paths can also include wildcard parameters:

Jex.create()
    .get("/path/*",
       ctx -> ctx.write("You are here because " + ctx.path() + " matches " + ctx.matchedPath())
       ));

Context

The Context object provides you with everything you need to handle a http-request. It contains the underlying JDK HttpExchange, and a bunch of convenience methods for extracting data from a request and sending a response.

See the Context Javadoc for a description of all the methods available

Filters

Filters are used to pre/post process incoming requests. Pre-processing occurs before the application's exchange handler is invoked, and post-processing occurs after the exchange handler returns. Filters are organised in chains, and are executed in the order they were registered. This can be useful for adding authentication, caching, extra logging, etc.

Each Filter in the chain invokes the next filter within its own filter(Context, FilterChain) implementation. The final Filter in the chain invokes the applications exchange handler..

Jex.create()
    .filter(
        (ctx, chain) -> {
          System.out.println("before request");
          // proceed to the next filter in the chain, or the endpoint handler if  at the end of
          // the chain
          chain.proceed();
          System.out.println("after request");
        });

The Filter may decide to terminate the chain, by not calling the method. In this case, the filter must send the response to the request, because the application's exchange handler will not be invoked.

Exceptions

Exception handlers give you a way of handling thrown exceptions during request processing;

var app = Jex.create();

app.error(NullPointerException.class, (ctx, e) -> {
    // handle nullpointers here
});

app.error(Exception.class, (ctx, e) -> {
    // handle general exceptions here
    // will not trigger if more specific exception handler found
});

HttpResponseException

Jex comes with a built in class called HttpResponseException, which can be used for default responses. If not caught by an exception handler, the exception is automatically converted into a response and sent to the user.

Access Management

When registering a route, you can specify security roles that can be accessed from within a request.

A common way to manage access is to use a filter as seen in the example below:

//custom enum for access roles
enum Access implements Role {
  USER,
  ADMIN
}

public static void main(String[] args) {
  Jex.create()
      .get("/user", ctx -> ctx.text("user"), Access.USER)
      .get("/admin", ctx -> ctx.text("admin"), Access.ADMIN)
      .filter(
          (ctx, chain) -> {
            // some user defined function that returns a user role
            Access userRole = getUserRole(ctx);
            // routeRoles are provided through the Context interface
            if (!ctx.routeRoles().contains(userRole)) {
              // request can be aborted by throwing an exception or by not calling chain.proceed()
              throw new HttpResponseException(403, "unauthorized");
            }
            chain.proceed();
          });
}

Json Content

Jex has a JsonService SPI to allow for (de)serialization from/to json. It requires only the toJson/fromJson methods to be implemented.

If jackson or avaje-jsonb is present on the class path, jex will create a default JsonService automatically if none are manually registered.

When a JsonService is available, we can use the Context methods bodyAsClass and bodyAsType to deserialize json and the json method to serialize and send a response back

Jex.create()
    .jsonService(new JacksonJsonService())
    .post(
        "/json",
        ctx -> {
          MyBody body = ctx.bodyAsClass(MyBody.class);
          ctx.json(new CustomResponse());
        });

Static Resources

There is a separate module for static resources that allows you to serve static resources from the classpath or filesystem.

It provides a StaticContent class to configure the location, http path of your static resources, and other attributes.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-jex-static-content</artifactId>
  <version>${avaje.jex.version}</version>
</dependency>
StaticContent singleFile =
    StaticContent.createFile("src/main/resources/example.txt").httpPath("/single").build();
StaticContent directoryCP =
    StaticContent.createCP("/public/").httpPath("/").directoryIndex("index.html").build();
Jex app =
    Jex.create()
        .plugin(singleFile) // will serve the src/main/resources/example.txt
        .plugin(directoryCP); // will serve files from the /public classpath directory