How to use multiple routers in Vertx

As soon as your application grows, you may need to increase a level of separation of components. Small projects can specify HTTP routes in a single place, however for more complex systems it would be a better idea to decompose API for each business domain. If you are moving from Spring, you know that there a single controller is an entry point for specific API group. In Vertx you can achieve similar goals using a concept of multirouting – that means you have one “application-level” router and many sub-routers, that are separated by domain. In this post we will observe how to implement this design pattern.

When you may need multiple routers?

When you perform a request to the server application, written with Vertx, it finds a first matching handler for the called route and then executes an associated code. The component, which is responsible for that, is called router. From a technical point of view, router is attached to the HttpServer instance and listens for all incoming requests on this server. It matches a request to the concrete handler, based on a set of criteria, which are defined by the Route class (e.g. method, path).

A small Vertx application (such as microservice or serverless function) basically can organize its components, based on a layer – such as, you could create a verticle to handle HTTP requests and a verticle to handle database operations. In this case, you could interact between these verticles using the EventBus messaging. In other words, all HTTP requests are handled by a single router, that is created inside the API verticle.

However, when you application grows, you may need to separate components, based on their business domain, rather on their technical layer. Imagine you work on a time tracking app, which may have separate verticles to work with projects, time entries, reports etc. So, if you will specify all routes inside a single router, it will soon become messy. Moreover, business verticles in this approach are coupled with the API verticle, and it would bring you a number of problems, when you would like to relocate it to separate microservices.

To make your life easier, you could define these business verticles as something similar to Spring controllers, so they would have their own routes groups. Once you assemble an application, these groups can be registered either with a “monolith” app verticle or with an own server (if you use each verticle as a foundation for a separate microservice). In Vertx this is possible using subrouters. Take a look on the graph below:

Essentially, we contrast there two described previously approaches. You could note, that on the left we use messaging in order to connect both verticles. On the contrary, for the second architecture we do not need messaging, as each verticle has its own router. Once you assemble an application, you only need to register these routers with the app router. In essence, verticles are not coupled to each other and this could be used as a first step to transmit from monolith to separate services.

Introducing an app router pattern

First thing first, we start to create an app router. As we reviewed early, it is a main router, that is used by the application’s HTTP server. We use it to register routers of each verticle. In addition, we could use it as a single entry point to configure there handlers, that are common for all verticles, such as CORS handlers, authentication handlers etc.

I would like to hide an actual router by the AppRouter interface and then use a dependency injection in order to provide this dependency to verticles. Check out the code snippet below, which defines this contract:

interface AppRouter {
    void addSubRouter(String path, Router subRouter);

    Router getAppRouter();
}

This inteface has a method, which registers a verticle’s router. We pass two arguments: a path prefix and an actual router. Also, we need to provide a method to receive a router’s reference. Now, let implement this interface:

public class AppRouterImpl implements AppRouter{

    private Router router;

    AppRouterImpl(Vertx vertx){
        router = Router.router(vertx);
    }

    @Override
    public void addSubRouter(String path, Router subRouter) {
       router.mountSubRouter(path, subRouter);
    }

    @Override
    public Router getAppRouter() {
        return router;
    }
}

The code is self explanatory. We create an instance of AppRouterImpl using the Vertx instance, created during deployment (in the actual assembly point of our application). When we register sub routers we call the addSubRouter() method and it will fire the native mountSubRouter() method.

The next step is to attach this router to the application’s server in the app verticle. Here we use the aforesaid getter method, because we need to obtain a router reference in order to attach it to a server. Take a look on the provided implementation:

class AppVerticle extends AbstractVerticle{

    @Inject
    private AppRouter appRouter;

    @Override
    public void start(Promise<Void> startPromise) throws Exception {
        Router router = appRouter.getAppRouter();
        HttpServer server = vertx.createHttpServer();
        server.requestHandler(router);
        server.listen(8080);
    }
    public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();
        //...
    }
}

Although, we can do much more inside the AppRouter component. As I have said already, we can use it in order to get common handlers configured, such as CORS handlers. In the next subsection we will observe how to do it.

Configure common handlers

We have mentioned already, that we can set up a shared functionality inside a single point. This will make business verticles more lightweight and at same time will eliminate a duplicate code. A good example of such common handlers can be a handler for CORS (stands for Cross Origin Resouce Sharing). To carry it out, Vertx includes a special CorsHanlder, that can be used with the router. What we have to do is following:

  1. Create a new Cors handler inside AppRouter
  2. Register a Cors handler instance as a part of a routing chain for all incoming requests

In order to obtain a new instance of the handler, we use a static factory method create(), which accepts a string with an allowed origin pattern. Let add this to the constructor of our router:

public class AppRouterImpl implements AppRouter{

    // ...

    // constructor
    AppRouterImpl(Vertx vertx){
        CorsHandler corsHandler = CorsHandler.create("*");
        corsHandler.allowedMethods(Set.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.GET, HttpMethod.DELETE));
        router = Router.router(vertx);
        router.route().handler(corsHandler);
    }
    // ...

Furthermore, we can configure the CorsHandler instance and define allowed HTTP methods and headers. In a similar manner, we could use the AppRouter to register any other shared handlers, such as a token authentication handler or a logging handler.

Setup verticles

Once we configured the main router we can inject it to verticles. To do that we will utilize the Google Guice framework, which perfecty accompanies Vertx. We will not go in details of how a dependency injection with Guice works – actually it is a topic for a separate post. But, in general, we need to complete three steps:

  1. Define a verticle dependencies
  2. Create a module with dependencies
  3. Create an instance with a module

A typical business verticle can have several shared dependencies, such as a main router, a database client, as well its own dependencies, e.g. various adapters, clients etc. We can prefix class dependencies with the @Inject annotation. In our case, all verticles have only one dependency:

public class ProjectsVerticle extends AbstractVerticle {
    @Inject
    private AppRouter appRouter;
    // ...

The next step here is to create a module. From a techical point of view, this class tells Guice how to inject dependencies into components. As our application is pretty simple, we could use one AppModule to provide dependencies. For more complex systems, you may need a separate module for each verticle. The only thing we need inside the module component is a reference of Vertx instance. We will use to create the AppRouter. Take a look on code snippet below:

class AppModule extends AbstractModule {

    private AppRouter router;

    AppModule(Vertx vertx){
        router = new AppRouterImpl(vertx);
    }
    @Override
    protected void configure() {
        bind(AppRouter.class).toInstance(router);
    }
}

Note, that we bind the interface dependency to the concrete instance, created inside the module. That allows us to re-use it both inside verticles (to mount sub routers) and inside the app verticle (to configure a server). Finally, we create verticles using the module. We can do it inside the main method of the application, like it is displayed in the code snippet below:

public class AppVerticle extends AbstractVerticle{

    /// ...
    public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();
        AppModule module = new AppModule(vertx);
        Injector injector = Guice.createInjector(module);
        ProjectsVerticle projectsVerticle = injector.getInstance(ProjectsVerticle.class);
        TimeEntriesVerticle timeEntriesVerticle = injector.getInstance(TimeEntriesVerticle.class);
        ReportsVerticle reportsVerticle = injector.getInstance(ReportsVerticle.class);
        AppVerticle appVerticle = injector.getInstance(AppVerticle.class);
        vertx.deployVerticle(projectsVerticle);
        vertx.deployVerticle(timeEntriesVerticle);
        vertx.deployVerticle(reportsVerticle);
        vertx.deployVerticle(appVerticle);
    }
}

Once we have obtained verticles, we deploy them using the Vertx instance. Straightaway, run the application and verify that everything works.

Conclusion

For small projects it is ok to keep HTTP routes in a single component (and it is a good implementation of the KISS principle too). As it gets bigger, you may need to separate routing into groups based on a business domain to increase testability and separation. That is how it is done in Spring using controllers. But it is also possible in Vertx using subrouters. In this post we observed how to implement a multirouters architecture for Vertx-based REST API.

admin Written by: