Fork me on GitHub

Routing

Ninja features one central route file. We think this is important, because you immediately can see what routes your application provides. It especially facilitates designing a nice RESTful application and API, because you have a good overview what is going on.

Basics

The route file is a plain old Java file living at conf/Routes.java.

Routes.java implements the interface ApplicationRoutes.java. This interface defines a method called public void init(Router router) {…} which allows us to map incoming requests to controllers and their methods.

public class Routes implements ApplicationRoutes {

    @Override
    public void init(Router router) {
        
        // a GET request to "index" will be handled by a class called
        // "AppController" its method "index".
        router.GET().route("/index").with(AppController::index);

        ...

    }
}

The init(…) method provides us with the Router and router allows us to define what happens for GET, POST, PUT, OPTIONS, HEAD and DELETE requests.

And if you want to route an HTTP method not yet supported by Ninja out of the box you can always use route.METHOD(“MY_CUSTOM_HTTP_METHOD”).route(…)….

Routes are matched top down. If an incoming request potentially maps to two routes, the route defined first is executed.

Java 8 routing with lambda expressions

Ninja 6 introduced support for routing using Java 8 lambda expressions. Ninja includes support for various kinds of lambda expressions including method references and anonymous lambdas. See https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html for more info.

The class ninja.ControllerMethods defines the various interfaces that are acceptable method signatures for Java 8 lambda expressions. A controller method returns a ninja.Result and has anywhere from 0 to 12 arguments. If you need more than 12 arguments, you can fallback to Ninja’s legacy routing strategy of Class + “method”.

The most common and recommended lambda expression is a reference to an instance method of an arbitrary type.

public class Routes implements ApplicationRoutes {

    @Override
    public void init(Router router) {
        
        router.GET().route("/").with(AppController::index);

    }
}

You can also use a lambda expression of a reference to an instance method of a particular object. Please note that Guice will not construct this instance and Ninja will use the one referenced instead.

public class Routes implements ApplicationRoutes {

    @Override
    public void init(Router router) {
            
        AppController controller = new AppController();
        router.GET().route("/").with(controller::index);

    }
}

You can also use a lambda expression of an anonymously defined method. The lambda method can take anywhere of 0 to 12 arguments. Please note that unlike methods defined in a regular class, Java 8 does not retain parameter annotations on anonymously defined lambda expressions. Ninja will still try to inject parameters via Guice into these methods, but it will only handle injection by type only. So while ninja.Context can be injected be aware that @Param(“a”) String a cannot until a future JDK addresses this limitation. We recommend using either zero arguments or ninja.Context with anonymously defined lambdas.

public class Routes implements ApplicationRoutes {

    @Override
    public void init(Router router) {
 
        router.GET().route("/").with(() -> Results.redirect("/home"));
        router.GET().route("/home").with((Context context) -> Results.ok());

    }
}

With result directly

You can use a Result object to render a static page simply without any controller.

public class Routes implements ApplicationRoutes {

    @Override
    public void init(Router router) {
        
        // a GET request to "/" will be redirect to "/dashboard"
        router.GET().route("/").with(() -> Results.redirect("/dashboard"));
        
        // show a static page
        router.GET().route("/dashboard").with(() -> Results.html().template("/dashboard.html"));

        ...

    }
}

Regex in your routes

Routes can contain arbitrary Java regular expressions. You can use these regular expressions simply by putting them into the route definition.

Some examples:

// matches for instance "/assets/00012", "/assets/12334", ...
router.GET().route("/assets/\\d*").with(AssetsController::serveDigits);

// matches for instance "/assets/myasset.xml", "/assets/boing.txt", ...
router.GET().route("/assets/.*").with(AssetsController::serveArbitrary);

In the first example \\d* tells the router to match digits (defined by \\d) of arbitrary length (defined by *). In the second example .* lets the router match arbitrary characters (.) of arbitrary length (defined by *).

This example also shows what happens if two routes match. For instance a request to /assets/00012 is matched by both route definitions. In that case the first matching route from top will be executed. In our case method serveDigits.

More on regular expressions: http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html

Getting variable URL parts into your controller method

A controller usually not only renders stuff, but also takes some inputs and does something with them.

Let’s assume we got a request like that:

GET /user/12345/[email protected]/userDashboard

Looks like a GET request to the userDashboard. The request seems to contain an id and an email. As application developer you want to have a convenient way to get id and email inside your controller. Ninja allows you to define and name parts of your routes via curly braces {…}:

router.GET().route("/user/{id}/{email}/userDashboard").with(ApplicationController::userDashboard);

We can then inject id and email into our controller via annotation @PathParam.

package controllers;

@Singleton
public class AppController {

    public Result userDashboard(
            @PathParam("id") String id, 
            @PathParam("email") String email) {

        //do something with the parameters...
    }

}
By default Ninja replaces curly braces with the following regex to match a variable part: ([^/]*). That means that variable parts match arbitrary characters but do not span over any path separators "/". If you need more control please you'll find more in the next section.

Regular expressions in variable route parts

Ninja allows you to specify regular expressions that will be used to match arbitrary parts of your URL.

The syntax is {PRAMETER_NAME: REGEX} (with a whitespace after the “:”).

For instance a route like /assets/{fileName: .*} will match everything after /assets/. Imagine a request to /assets/css/app.css. This request will be handled by the route and accessing the path parameter fileName will return css/app.css.

Routes can contain multiple variable parts with regular expressions.

For example, for a request to /categories/1234/products/5678, where category is expected to be an integer value and product to be either integer or string value, you can define routes like that:

router.GET().route("/categories/{catId: [0-9]+}/products/{productId: [0-9]+}").with(ProductController::product);
router.GET().route("/categories/{catId: [0-9]+}/products/{productName: .*}").with(ProductController::productByName);

The request above will be handled by the first route, and request to /categories/1234/products/mouse will be handled by the second route.

Values of variable parts of a route are injected into our controller (explained above) and are implicitly validated with regular expressions. So our controller for routes above would be like that (look at @PathParam argument types):

package controllers;

@Singleton
public class ProductController {

    public Result product(
            @PathParam("catId") int catId, 
            @PathParam("productId") int productId) {

        // find product by id in given category 
    }

    public Result productByName(
            @PathParam("catId") int catId, 
            @PathParam("productName") String productName) {

        // find product(s) by name in given category 
    }
}

Note at how regular expressions in routes can be used to validate path parameters and to define fine grained routing.

You can use any Java compliant regular expressions for matching. Please refer to the official documentation at http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html.

Injecting objects into the Router

Because the Route.java is just a plain old java file we can also inject arbitrary stuff into it (via Guice).

A popular use case is to inject ninjaProperties. This allows us to activate / deactivate certain routes based on the environment we are in.

The following example shows that a setup route is not available when running in production:

public class Routes implements ApplicationRoutes {
    
    @Inject
    NinjaProperties ninjaProperties;

    @Override
    public void init(Router router) {
        
        // a GET request to "index" will be handled by a class called
        // "AppController" its method "index".
        router.GET().route("/index").with(AppController::index);

        ...

        // only active when not in production mode:
        if (!ninjaProperties.isProd) {
            router.GET().route("/setup").with(AppController::setup);
        }
    }
}

Reverse routing

Let’s say you want to know the final route of a class ApplicationController.class, method “index”. Assume that the original raw route looked like “/”.

You can get the final URL by injecting a ninja.ReverseRouter into your controller and calling various with methods to lookup the route and optionally replace path or add query string parameters.

@Inject
ReverseRouter reverseRouter;

public void myMethod() {

    // will result into "/"
    String url = reverseRouter.with(ApplicationController::index)
        .build();
    
    // url would be "/"

}      

Now consider a more complex example. Say the original raw route contained placeholders on the following form: /user/{id}/{email}/userDashboard. You can now ask the router for the final URL and swap in path parameters in addition to adding query string parameters.

@Inject
ReverseRouter reverseRouter;

public void myMethod() {

    String url = reverseRouter.with(ApplicationController::userDashboard)
        .path("id", "123")
        .path("email", "[email protected]")
        .query("paging_size", 100)
        .query("page", 1)
        .build();

    // url will be "/user/123/test%40example.com/userDashboard?paging_size=100&page=1"
}      
The `ninja.ReverseRouter` will validate that all path parameters were set or will throw an `IllegalArgumentException` while building. All values are URL-escaped by default or various `raw` methods can be used instead to directly build your final URL.

Redirecting a user to a reverse route is a common use case. The Builder object returned by the reverse router has a handy redirect() method to build a ninja.Result that will redirect the user.

@Inject
ReverseRouter reverseRouter;

public Result myMethod() {

    // will redirect to "/"
    return reverseRouter.with(ApplicationController::index)
        .redirect();

}      

A note on encoding / decoding

Encoding / Decoding of URLs is not as easy as you might think it is. Ninja tries to simplify everything as much as possible, but as a user of the API you have to know what you are submitting to Ninja.

We recommend the following excellent article from Lunatech before you use encoding / decoding actively in your application.

Let’s reconsider the controller method from above:

package controllers;

@Singleton
public class ApplicationController {

    public Result index(
            @PathParam("id") String id, 
            @PathParam("email") String email, 
            @Param("debug") String debug) {

        //do something with the parameters...
    }

}

You can expect that String id and String debug are both correctly decoded values. BUT This assumes that you are encoding the values correctly on the client side. And encoding is different for query parameters or stuff in the path. Do not even think about using URLEncoder for encoding URLs. This is wrong.

Simple example that outlines some of the difficulties: Think of a route “/user/{id}/userDashboard”.

Let’s say your id is “rootuser/domain”. If you do not encode the slash in the middle you end up with a URL like /user/rootuser/domain/userDashboard. And the this URL does not match the route because of the “/”.

Therefore you have to encode your id correctly. In that case it would be: rootuser%2Fdomain. When the user then visits /user/rootuser%2Fdomain/userDashboard the route matches and a @PathParam(“id”) would then be rootuser/domain as it is decoded by Ninja.

In principle it is really simple. But it is even simpler to mess encoding / decoding up. The article from Lunatech mentioned earlier is awesome and explains everything.

JAX-RS-style Annotated Routes in Ninja (optional)

Ninja-jaxy-routes allows you to register your routes using annotations similar to JAX-RS.

You may use the standard Ninja route registration in combination with this route builder or you may replace all your route registrations with annotations.

NOTE: Your annotated controllers must be located somewhere within your application’s configured controller package or a subpackage thereof.

Add the ninja-jaxy-routes dependency

<dependency>
    <groupId>org.ninjaframework</groupId>
    <artifactId>ninja-jaxy-routes</artifactId>
    <version>${ninja.version}</version>
</dependency>

Initialize JaxyRoutes in your conf.Routes class.

@Inject
JaxyRoutes jaxyRoutes;

@Override
public void init(Router router) {

    jaxyRoutes.init(router);
    
}

Annotate Your Controllers

Now you are ready to start annotating your controllers.

Paths

Ninja-jaxy-routes supports multiple @Path specs per controller method and also supports controller class inheritance.

The following example will register two GET routes /base/middle/app/get and /base/middle/app/retrieve for the same controller method.

@Path("/base")
class Base {
}

@Path("/middle")
class Middle extends Base {
}

@Path("/app")
class App extends Middle {

    @Path({"/get", "/retrieve"})
    @GET
    Result get() {
        return Results.text().renderRaw("Yahoo!"); 
    }        
}

If the Base and Middle parent classes had each specified multiple paths, all permutations of the complete routes would be registered too.

HTTP Methods

By default, all routes are assumed to be GET routes unless they are specifically annotated.

The following common HTTP method annotations are available:

  • @DELETE
  • @GET
  • @HEAD
  • @OPTIONS
  • @PATCH
  • @POST
  • @PUT

If the built-in methods are insufficient :

  1. Set the key ninja.jaxy.custom_http_methods to true in your conf/application.conf.
  2. Then implement your own custom HTTP method:

    @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @HttpMethod(“CUSTOM”) public @interface CUSTOM { }

Registration Order

Since your controllers and methods are reflectively loaded we can not expect a predictable route registration order from the JVM.

To compensate for this, you may specify the @Order annotation on a controller method to dictate the ordering of routes.

  1. It is not necessary to specify an @Order for every method.
  2. Lower numbers are registered first, higher numbers later.
  3. If two methods have the same order, the registration order is determined by String comparison of the complete method address (controller+method).

Here is an example of specifying an @Order.

@Path({"/get", "/retrieve"})
@GET
@Order(10)
Result something() {
}

Runtime Mode Inclusions/Exclusions

You may include/exclude routes based on the Ninja runtime mode.

  1. If no mode annotations are specified, then the route is available in all modes.
  2. You may specify multiple mode annotations on a controller method.
  • @Dev
  • @Prod
  • @Test

Here is an example of specifying @Dev and @Test.

@Path("/diagnostics")
@GET
@Dev @Test
Result diagnostics() {
}

NinjaProperties Inclusions/Exclusions

It is also possible to include/exclude a route based on a NinjaProperties key.

If the key does not exist in your runtime config, the route is not registered.

@Path("/sneaky")
@GET
@Requires("sneaky.key")
Result somethingSneaky() {
}