JavaScript Routing in Play! Framework

I was working on a Play! Framework project and I was wondering why our whole API was available in one piece. It got me thinking whether there is a way to hide at least some parts of the API from the clients. Precisely because some of the clients didn’t have the privilege/permission to execute given operations, e.g. visiting restricted pages. I thought there should be a way to hide at least the operations that are on these restricted pages. I’ve found a way to do so and below it is.

text

The original structure

You might know we develop with Play! framework. Usually we use the routing system provided by the framework. In several cases we have used the Twirl template engine which is a built-in feature of the framework. We also use AngularJS on the client-side. The frontend logic needed the JavaScript routing.

We had a controller for each business object, so most of the views were served from a few huge classes with all the logic implemented in those.

The provided routing had to be made accessible to the frontend. Creating an API call is as follows:

package controllers;
  
import play.Routes;
import play.mvc.Controller;
import play.mvc.Result;
  
public class Application extends Controller {
  
    // ...
     
    public static Result jsRoutes() {
        response().setContentType("text/javascript");
        return ok(
                Routes.javascriptRouter("jsRoutes",
                routes.javascript.Application.index(),
                routes.javascript.PetCtrl.getPet()
                //...
                //many more Controllers and functions
                )
        );
    }
     
    // ...
  
}

Making it accessible from the frontend.

GET /assets/js/routes controllers.Application.jsRoutes()

And in our main.scala.html:

<script type="text/javascript" src="@routes.Application.jsRoutes()"></script>

Usage from your code

$http.get(jsRoutes.controllers.PetCtrl.getPet(petId).url)

In this case you just made every url accessible from your client-side/frontend. There are a few problems with this approach:

Shrinking the length of the jsRoutes method/function

First of all we changed how we separate functionalities. Each view had its own controller with a service specified for the specific functionality and common service for the common stuff. We moved the routing to each controller, but collected it into one huge jsRoutes.js as before. In this case the Application.java has been modified.

public static Result jsRoutes() {
   response().setContentType("text/javascript");
   return ok(Routes.javascriptRouter("jsRoutes", generateJavascriptRouting()));
}
private static JavascriptReverseRoute[] generateJavascriptRouting() {
    ArrayList<JavascriptReverseRoute> routeList = new ArrayList<>();
    routeList.addAll(PetCtrl.getRoutes());
    //...
    return routeList.toArray(new JavascriptReverseRoute[routeList.size()]);
}

All the Controllers had the same getRoutes() method which returns the list of the public methods used from javascript code.

public static List<Router.JavascriptReverseRoute> getRoutes() {
    ArrayList<Router.JavascriptReverseRoute> routeList = new ArrayList<>();
    routeList.add(controllers.routes.javascript.PetCtrl.getPet());
    return routeList;
}

Your full API is still available in one piece, but the code maintenance has been decreased drastically because you can find all your code where it belongs locally.

[Update: As Csaba Ujvári pointed out to make this work in Play 2.4+ you have to use JavaScriptReserveRoute instead of JavascriptReserveRoute]

Creating jsRoutes for each view

The next step is to reduce the surface of your observable interface at a time. If you prefer this case, you need a little ceremony to create your implementation. First of all you need to create a new line in your routes file for each view.

GET /assets/js/pets/view/routes controllers.PetCtrl.jsRoutes()

Each of your Controller file should be modified a little:

public class PetCtrl extends Controller {
    public static Result jsRoutes() {
            response().setContentType("text/javascript");
        return ok(Routes.javascriptRouter("petRoutes", generateJavascriptRouting()));
    }
     
    private static JavascriptReverseRoute[] generateJavascriptRouting() {
            ArrayList<JavascriptReverseRoute> routeList = new ArrayList<>();
        routeList.addAll(PetCtrl.getRoutes());
        // ...
        return routeList.toArray(new JavascriptReverseRoute[routeList.size()]);
    }
    // ...
}

Your view should download this routing for every view but it contains only the required functionality.

<script type="text/javascript" src="@routes.PetCtrl.jsRoutes()"></script>

You can call them like this:

$http.get(petRoutes.controllers.PetCtrl.getPet(petId).url)

As you can see (petRoutes) you can specify the “namespace” with this approach.

Summary

It costs you only a short ceremony to:

I hope this will prove to be a useful resource to many of my fellow backend developers! If you have any questions or related solutions, let me know! :)

[Editor’s note: to learn more about the author, visit our Facebook page.]

member photo

Never underestimate the capabilities of a lazy person.

Latest post by Barnabás Oláh

Deploying Your Play! Framework App to Azure as a PaaS