Nima

Discord Source API Docs Issues Releases
Discord GitHub Javadoc GitHub



A combination of Helidon SE Webserver and Avaje libraries, includes:

Maven dependencies

1. Add avaje-nima dependencies

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

<!-- test dependency -->
<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-nima-test</artifactId>
  <version>${avaje.nima.version}</version>
  <scope>test</scope>
</dependency>

2. Add the annotation processor

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.14.0</version>
      <configuration>
        <!-- Annotation processors -->
        <annotationProcessorPaths>
          <path>
            <groupId>io.avaje</groupId>
            <artifactId>avaje-nima-generator</artifactId>
            <version>${avaje-nima.version}</version>
          </path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
  </plugins>
</build>

Note the avaje-nima-generator is actually a composite of:

Controller

Create a package (e.g. org.example.web) and create you first controller in the package.

package org.example.web;

import io.avaje.http.api.*;
import io.avaje.inject.*;
import io.avaje.jsonb.Json;

@Controller
public class HelloController {

  @Produces("text/plain")
  @Get("/")
  String hello() {
    return "hello world";
  }

  @Get("/json")
  HelloBean one() {
    return new HelloBean(97, "Hello JSON");
  }

  @Json
  public record HelloBean(int id, String name) {

  }
}

Generated code

After compiling HelloController we should see in target/generated-sources/annotations:

Generated test code

And for test purposes in target/generated-test-sources/test-annotations a couple of things are generated for us:

Test client - HelloControllerTestAPI

We can use the generated test client to create a component test, and test our controller. The name of the generated test clients is: {controllerName} + TestAPI.

Controller component test

Create a test class called HelloControllerTest to test the controller.

package org.example.web;

import io.avaje.inject.test.InjectTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;

import java.net.http.HttpResponse;

import static org.assertj.core.api.Assertions.assertThat;

@InjectTest
class HelloControllerTest {

  @Inject HelloControllerTestAPI client;

  @Test
  void hello() {

    HttpResponse<String> res = client.hello();
    assertThat(res.statusCode()).isEqualTo(200);
    assertThat(res.body()).isEqualTo("hello world");
  }
}

Main method

Create a main method to run the application.

package org.example;

import io.avaje.nima.Nima;

public class Main {

  public static void main(String[] args) {

    var webServer = Nima.builder()
      .port(8080)
      .build();

    webServer.start();
  }
}

Now you can run the application main method,

21:23:37.951 [main] INFO  io.avaje.inject - Wired beans in 77ms
21:23:38.001 [features-thread] INFO  i.h.common.features.HelidonFeatures - Helidon SE 4.2.2 features: [Config, Encoding, Media, Metrics, Registry, WebServer]
21:23:38.005 [start @default (/0.0.0.0:8080)] INFO  io.helidon.webserver.ServerListener - [0x2132b530] http://0.0.0.0:8080 bound for socket '@default'
21:23:38.008 [main] INFO  io.helidon.webserver.LoomServer - Started all channels in 6 milliseconds. 369 milliseconds since JVM startup. Java 21.0.5+9-LTS-239
  

... and perform a quick test using curl.

curl http://localhost:8080
hello world

curl http://localhost:8080/json
{"id":97,"name":"Hello JSON"}

Error handlers

Commonly we want define all the global exception handling in a single exception handler. We can do this by creating a controller than just has @ExceptionHandler methods.

An example ErrorHandlers controller is:

package org.example.web;

import io.avaje.http.api.Controller;
import io.avaje.http.api.ExceptionHandler;
import io.avaje.http.api.Produces;
import io.helidon.http.BadRequestException;
import io.helidon.webserver.http.ServerRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.UUID;

@Controller
final class ErrorHandlers {

  private static final Logger log = LoggerFactory.getLogger(ErrorHandlers.class);

  @Produces(statusCode = 500)
  @ExceptionHandler
  ErrorResponse defaultError(Exception ex, ServerRequest req) {
    var path = path(req);
    var traceId = log(ex, path);
    return ErrorResponse.builder()
      .statusCode(500)
      .path(path)
      .traceId(traceId)
      .message("Unhandled server error")
      .build();
  }

  @Produces(statusCode = 400)
  @ExceptionHandler
  ErrorResponse badRequest(BadRequestException ex, ServerRequest req) {
    var path = path(req);
    var traceId = log(ex, path);
    return ErrorResponse.builder()
      .statusCode(400)
      .path(path)
      .traceId(traceId)
      .message("Unhandled server error")
      .build();
  }

  private static String path(ServerRequest req) {
    return req != null && req.path() != null ? req.path().path() : null;
  }

  private static UUID log(Throwable ex, String path) {
    UUID traceId = UUID.randomUUID();
    log.error("Unhandled server error path:{} trace:{}", path, traceId, ex);
    return traceId;
  }

}

With the above example the error response has a JSON payload. The code for the ErrorResponse is:

import io.avaje.jsonb.Json;
import io.avaje.recordbuilder.RecordBuilder;

import java.util.UUID;

@Json
@RecordBuilder
public record ErrorResponse(

  int statusCode,
  String path,
  UUID traceId,
  String message) {

  public static ErrorResponseBuilder builder() {
    return ErrorResponseBuilder.builder();
  }
}
Generated Code: (click to expand)
@Generated("avaje-helidon-generator")
@Component
public final class ErrorHandlers$Route implements HttpFeature {

  private final ErrorHandlers controller;
  private final JsonType<ErrorResponse> errorResponseJsonType;

  public ErrorHandlers$Route(ErrorHandlers controller, Jsonb jsonb) {
    this.controller = controller;
    this.errorResponseJsonType = jsonb.type(ErrorResponse.class);
  }

  @Override
  public void setup(HttpRouting.Builder routing) {
    routing.error(Exception.class, this::_defaultError);
    routing.error(BadRequestException.class, this::_badRequest);
  }

  private void _defaultError(ServerRequest req, ServerResponse res, Exception ex) {
    res.status(INTERNAL_SERVER_ERROR_500);
    var result = controller.defaultError(ex, req);
    if (result == null) {
      res.status(NO_CONTENT_204).send();
    } else {
      res.headers().contentType(MediaTypes.APPLICATION_JSON);
      errorResponseJsonType.toJson(result, JsonOutput.of(res));
    }
  }

  private void _badRequest(ServerRequest req, ServerResponse res, BadRequestException ex) {
    res.status(BAD_REQUEST_400);
    var result = controller.badRequest(ex, req);
    if (result == null) {
      res.status(NO_CONTENT_204).send();
    } else {
      res.headers().contentType(MediaTypes.APPLICATION_JSON);
      errorResponseJsonType.toJson(result, JsonOutput.of(res));
    }
  }

}

Filters

To add a Filter we can add a controller that has a @Filter method that takes a FilterChain, RoutingRequest and optionally a RoutingResponse.

An example filter that reads a "Caller-Id" request header and rejects the request if one isn't provided.

import io.avaje.http.api.Controller;
import io.avaje.http.api.Filter;
import io.helidon.http.BadRequestException;
import io.helidon.http.HeaderName;
import io.helidon.http.HeaderNames;
import io.helidon.webserver.http.FilterChain;
import io.helidon.webserver.http.RoutingRequest;
import io.helidon.webserver.http.ServerRequest;

import java.util.Optional;
import java.util.Set;

@Controller
final class CallerIdFilter {

  private static final HeaderName CALLER_ID = HeaderNames.create("Caller-Id");

  private static final Set<String> BYPASS = Set.of("/ping");

  @Filter
  void filter(FilterChain chain, RoutingRequest req) {
    var path = path(req);
    if (BYPASS.contains(path)) {
      chain.proceed();
    } else {
      String callerId = callerId(req).orElseThrow(() -> new BadRequestException("Caller-Id required"));
      handleCallerMetrics(path, callerId);
      chain.proceed();
    }
  }

  private void handleCallerMetrics(String path, String callerId) {
    // capture metrics
  }

  private Optional<String> callerId(RoutingRequest request) {
    return request.headers().first(CALLER_ID);
  }


  private static String path(ServerRequest req) {
    return req != null && req.path() != null ? req.path().path() : null;
  }

}