Skip to main content

Some Rest with Vert.x

Previously in this blog series

This post is part of the Introduction to Vert.x series. So, let’s have a quick look about the content of the previous posts. In the first post, we developed a very simple Vert.x 3 application, and saw how this application can be tested, packaged and executed. In the last post, we saw how this application became configurable and how we can use a random port in test.

Well, nothing fancy… Let’s go a bit further this time and develop a CRUD-ish application. So an application exposing an HTML page interacting with the backend using a REST API. The level of RESTfullness of the API is not the topic of this post, I let you decide as it’s a very slippery topic.

So, in other words we are going to see:

  • Vert.x Web - a framework that let you create Web applications easily using Vert.x
  • How to expose static resources
  • How to develop a REST API

The code developed in this post is available on the post-3 branch of this Github project. We are going to start from the post-2 codebase.

So, let’s start.

Vert.x Web

As you may have notices in the previous posts, dealing with complex HTTP application using only Vert.x Core would be kind of cumbersome. That’s the main reason behind Vert.x Web. It makes the development of Vert.x base web applications really easy, without changing the philosophy.

To use Vert.x Web, you need to update the pom.xml file to add the following dependency:

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-web</artifactId>
  <version>3.0.0</version>
</dependency>

That’s the only thing you need to use Vert.x Web. Sweet, no ?

Let’s now use it. Remember, in the previous post, when we requested http://localhost:8080, we reply a nice Hello World message. Let’s do the same with Vert.x Web. Open the io.vertx.blog.first.MyFirstVerticle class and change the start method to be:

@Override
public void start(Future<Void> fut) {
 // Create a router object.
 Router router = Router.router(vertx);

 // Bind "/" to our hello message - so we are still compatible.
 router.route("/").handler(routingContext -> {
   HttpServerResponse response = routingContext.response();
   response
       .putHeader("content-type", "text/html")
       .end("<h1>Hello from my first Vert.x 3 application</h1>");
 });

 // Create the HTTP server and pass the "accept" method to the request handler.
 vertx
     .createHttpServer()
     .requestHandler(router::accept)
     .listen(
         // Retrieve the port from the configuration,
         // default to 8080.
         config().getInteger("http.port", 8080),
         result -> {
           if (result.succeeded()) {
             fut.complete();
           } else {
             fut.fail(result.cause());
           }
         }
     );
}

You may be surprise by the length of this snippet (in comparison to the previous code). But as we are going to see, it will make our app on steroids, just be patient.

As you can see, we start by creating a Router object. The router is the cornerstone of Vert.x Web. This object is responsible for dispatching the HTTP requests to the right handler. Two other concepts are very important in Vert.x Web:

  • Routes - which let you define how request are dispatched
  • Handlers - which are the actual action processing the requests and writing the result. Handlers can be chained.

If you understand these 3 concepts, you have understood everything in Vert.x Web.

Let’s focus on this code first:

router.route("/").handler(routingContext -> {
  HttpServerResponse response = routingContext.response();
  response
      .putHeader("content-type", "text/html")
      .end("<h1>Hello from my first Vert.x 3 application</h1>");
});

It routes requests arriving on “/“ to the given handler. Handlers receive a RoutingContext object. This handler is quite similar to the code we had before, and it’s quite normal as it manipulates the same type of object: HttpServerResponse.

Let’s now have a look to the rest of the code:

vertx
    .createHttpServer()
    .requestHandler(router::accept)
    .listen(
        // Retrieve the port from the configuration,
        // default to 8080.
        config().getInteger("http.port", 8080),
        result -> {
          if (result.succeeded()) {
            fut.complete();
          } else {
            fut.fail(result.cause());
          }
        }
    );
}

It’s basically the same code as before, except that we change the request handler. We pass router::accept to the handler. You may not be familiar with this notation. It’s a reference to a method (here the method accept from the router object). In other worlds, it instructs vert.x to call the accept method of the router when it receives a request.

Let’s try to see if this work:

mvn clean package
java -jar target/my-first-app-1.0-SNAPSHOT-fat.jar

By opening http://localhost:8080 in your browser you should see the Hello message. As we didn’t change the behavior of the application, our tests are still valid.

Exposing static resources

Ok, so we have a first application using vert.x web. Let’s see some of the benefits. Let’s start with serving static resources, such as an index.html page. Before we go further, I should start with a disclaimer: “the HTML page we are going to see here is ugly like hell : I’m not a UI guy”. I should also add that there are probably plenty of better ways to implement this and a myriad of frameworks I should try, but that’s not the point. I tried to keep things simple and just relying on JQuery and Twitter Bootstrap, so if you know a bit of JavaScript you can understand and edit the page.

Let’s create the HTML page that will be the entry point of our application. Create an index.html page in src/main/resources/assets with the content from here. As it’s just a HTML page with a bit of JavaScript, we won’t detail the file here. If you have questions, just post comments.

Basically, the page is a simple CRUD UI to manage my collection of not-yet-finished bottles of Whisky. It was made in a generic way, so you can transpose it to your own collection. The list of product is displayed in the main table. You can create a new product, edit one or delete one. These actions are relying on a REST API (that we are going to implement) through AJAX calls. That’s all.

Once this page is created, edit the io.vertx.blog.first.MyFirstVerticle class and change the start method to be:

@Override
public void start(Future<Void> fut) {
 Router router = Router.router(vertx);
 router.route("/").handler(routingContext -> {
   HttpServerResponse response = routingContext.response();
   response
       .putHeader("content-type", "text/html")
       .end("<h1>Hello from my first Vert.x 3 application</h1>");
 });

 // Serve static resources from the /assets directory
 router.route("/assets/*").handler(StaticHandler.create("assets"));

 vertx
     .createHttpServer()
     .requestHandler(router::accept)
     .listen(
         // Retrieve the port from the configuration,
         // default to 8080.
         config().getInteger("http.port", 8080),
         result -> {
           if (result.succeeded()) {
             fut.complete();
           } else {
             fut.fail(result.cause());
           }
         }
     );
}

The only difference with the previous code is the router.route("/assets/*").handler(StaticHandler.create("assets")); line. So, what does this line mean? It’s actually quite simple. It routes requests on “/assets/*” to resources stored in the “assets” directory. So our index.html page is going to be served using http://localhost:8080/assets/index.html.

Before testing this, let’s take a few seconds on the handler creation. All processing actions in Vert.x web are implemented as handler. To create a handler you always call the create method.

So, I’m sure you are impatient to see our beautiful HTML page. Let’s build and run the application:

mvn clean package
java -jar target/my-first-app-1.0-SNAPSHOT-fat.jar

Now, open your browser to http://localhost:8080/assets/index.html. Here it is… Ugly right? I told you.

As you may notice too… the table is empty, this is because we didn’t implement the REST API yet. Let’s do that now.

REST API with Vert.x Web

Vert.x Web makes the implementation of REST API really easy, as it basically routes your URL to the right handler. The API is very simple, and will be structured as follows:

  • GET /api/whiskies => get all bottles (getAll)
  • GET /api/whiskies/:id => get the bottle with the corresponding id (getOne)
  • POST /api/whiskies => add a new bottle (addOne)
  • PUT /api/whiskies/:id => update a bottle (updateOne)
  • DELETE /api/whiskies/id => delete a bottle (deleteOne)

We need some data…

But before going further, let’s create our data object. Create the src/main/java/io/vertx/blog/first/Whisky.java with the following content:

package io.vertx.blog.first;

import java.util.concurrent.atomic.AtomicInteger;

public class Whisky {

  private static final AtomicInteger COUNTER = new AtomicInteger();

  private final int id;

  private String name;

  private String origin;

  public Whisky(String name, String origin) {
    this.id = COUNTER.getAndIncrement();
    this.name = name;
    this.origin = origin;
  }

  public Whisky() {
    this.id = COUNTER.getAndIncrement();
  }

  public String getName() {
    return name;
  }

  public String getOrigin() {
    return origin;
  }

  public int getId() {
    return id;
  }

  public void setName(String name) {
    this.name = name;
  }

  public void setOrigin(String origin) {
    this.origin = origin;
  }
}

It’s a very simple bean class (so with getters and setters). We choose this format because Vert.x is relying on Jackson to handle the JSON format. Jackson automates the serialization and deserialization of bean classes, making our code much simpler.

Now, let’s create a couple of bottles. In the MyFirstVerticle class, add the following code:

// Store our product
private Map<Integer, Whisky> products = new LinkedHashMap<>();
// Create some product
private void createSomeData() {
  Whisky bowmore = new Whisky("Bowmore 15 Years Laimrig", "Scotland, Islay");
  products.put(bowmore.getId(), bowmore);
  Whisky talisker = new Whisky("Talisker 57° North", "Scotland, Island");
  products.put(talisker.getId(), talisker);
}

Then, in the start method, call the createSomeData method:

@Override
public void start(Future<Void> fut) {

  createSomeData();

  // Create a router object.
  Router router = Router.router(vertx);

  // Rest of the method
}

As you have noticed, we don’t really have a backend here, it’s just a (in-memory) map. Adding a backend will be covered by another post.

Get our products

Enough decoration, let’s implement the REST API. We are going to start with GET /api/whiskies. It returns the list of bottles in a JSON Array.

In the start method, add this line just below the static handler line:

router.get("/api/whiskies").handler(this::getAll);

This line instructs the router to handle the GET requests on “/api/whiskies” by calling the getAll method. We could have inlined the handler code, but for clarity reasons let’s create another method:

private void getAll(RoutingContext routingContext) {
  routingContext.response()
      .putHeader("content-type", "application/json; charset=utf-8")
      .end(Json.encodePrettily(products.values()));
}

As every handler our method receives a RoutingContext. It populates the response by setting the content-type and the actual content. Because our content may contain weird characters, we force the charset to UTF-8. To create the actual content, no need to compute the JSON string ourself. Vert.x lets us use the Json API. So Json.encodePrettily(products.values()) computes the JSON string representing the set of bottles.

We could have used Json.encodePrettily(products), but to make the JavaScript code simpler, we just return the set of bottles and not an object containing ID => Bottle entries.

With this in place, we should be able to retrieve the set of bottle from our HTML page. Let’s try it:

mvn clean package
java -jar target/my-first-app-1.0-SNAPSHOT-fat.jar

Then open the HTML page in your browser (http://localhost:8080/assets/index.html), and should should see:

I’m sure you are curious, and want to actually see what is returned by our REST API. Let’s open a browser to http://localhost:8080/api/whiskies. You should get:

[ {
  "id" : 0,
  "name" : "Bowmore 15 Years Laimrig",
  "origin" : "Scotland, Islay"
}, {
  "id" : 1,
  "name" : "Talisker 57° North",
  "origin" : "Scotland, Island"
} ]

Create a product

Now we can retrieve the set of bottles, let’s create a new one. Unlike the previous REST API endpoint, this one need to read the request’s body. For performance reason, it should be explicitly enabled. Don’t be scared… it’s just a handler.

In the start method, add these lines just below the line ending by getAll:

router.route("/api/whiskies*").handler(BodyHandler.create());
router.post("/api/whiskies").handler(this::addOne);

The first line enables the reading of the request body for all routes under “/api/whiskies”. We could have enabled it globally with router.route().handler(BodyHandler.create()).

The second line maps POST requests on /api/whiskies to the addOne method. Let’s create this method:

private void addOne(RoutingContext routingContext) {
  final Whisky whisky = Json.decodeValue(routingContext.getBodyAsString(),
      Whisky.class);
  products.put(whisky.getId(), whisky);
  routingContext.response()
      .setStatusCode(201)
      .putHeader("content-type", "application/json; charset=utf-8")
      .end(Json.encodePrettily(whisky));
}

The method starts by retrieving the Whisky object from the request body. It just reads the body into a String and passes it to the Json.decodeValue method. Once created it adds it to the backend map and returns the created bottle as JSON.

Let’s try this. Rebuild and restart the application with:

mvn clean package
java -jar target/my-first-app-1.0-SNAPSHOT-fat.jar

Then, refresh the HTML page and click on the Add a new bottle button. Enter the data such as: “Jameson” as name and “Ireland” as origin (purists would have noticed that this is actually a Whiskey and not a Whisky). The bottle should be added to the table.

Status 201 ?
As you can see, we have set the response status to 201. It means CREATED, and is the generally used in REST API that create an entity. By default vert.x web is setting the status to 200 meaning OK.

Finishing a bottle

Well, bottles do not last forever, so we should be able to delete a bottle. In the start method, add this line:

router.delete("/api/whiskies/:id").handler(this::deleteOne);

In the URL, we define a path parameter :id. So, when handling a matching request, Vert.x extracts the path segment corresponding to the parameter and let us access it in the handler method. For instance, /api/whiskies/0 maps id to 0.

Let’s see how the parameter can be used in the handler method. Create the deleteOne method as follows:

private void deleteOne(RoutingContext routingContext) {
  String id = routingContext.request().getParam("id");
  if (id == null) {
    routingContext.response().setStatusCode(400).end();
  } else {
    Integer idAsInteger = Integer.valueOf(id);
    products.remove(idAsInteger);
  }
  routingContext.response().setStatusCode(204).end();
}

The path parameter is retrieved using routingContext.request().getParam("id"). It checks whether it’s null (not set), and in this case returns a Bad Request response (status code 400). Otherwise, it removes it from the backend map.

Status 204 ?
As you can see, we have set the response status to 204 - NO CONTENT. Response to the HTTP Verb delete have generally no content.

The other methods

We won’t detail getOne and updateOne as the implementations are straightforward and very similar. Their implementations are available on GitHub.

Cheers !

It’s time to conclude this post. We have seen how Vert.x Web lets you implement a REST API easily and how it can serve static resources. A bit more fancy than before, but still pretty easy.

In the next post we are going to improve our tests to cover the REST API.

Say Tuned & Happy Coding !