Playframework Routes – Part 1, Basics

Play Routes - Part 1, Basics

Routes? What’s to explain?

Routes in Play are not a difficult concept to grasp, but as always, the devil is in the details. Let me show you how to get the most out of routes and how to tackle some of the typical problems in designing the routing scheme of a Play web application.

Defining routes

Before we dive into the details, let’s recap the basics.

A route is a mapping from a URL to a controller action. Routes are defined in a Play project’s routes file in the conf folder. You can only use one routes file and routes in submodules are currently not supported (but will be in the upcoming 2.1 release).

A route is defined by specifying the HTTP method (Play supports GET, PUT, POST, DELETE), the path, and a fully-qualified controller method (usually called an action). These three elements are separated by as much whitespace as you like.

# Show the index page
GET     /                 controllers.Application.index()
# Login
POST    /login            controllers.Application.login()

Routes are evaluated from top to bottom, i.e. first come, first served. This is important in your route design, because if you’re not careful, routes can override each other.

On thing that Play doesn’t handle well is routes that end with a slash. In the Unix/Mac file system, /Applications and /Applications/ would be considered equal. On the web, these could be different. Since this only creates confusion, you should choose one layout and stick to it. You can then redirect All the other URLs to your preferred format, either in the routes file or better in the Global object (see last section).

GET /users   controllers.Users.showAllUsers()
GET /users/  controllers.Default.redirectTo("/users")

Parameters

In a web application, some parts of the URL are dynamic in nature. The path of the URL indicates a resource, while the query String provides options for the resource (most of the time HTML form values). In Play, you can map parameters to both the path as well as the query string.

You define path parameters using a colon, e.g. :username or :id, and map them by name (either as-is or with a regular expression) to parameters in the controller action. The controller action is written in Scala-like syntax, i.e. parameters are defined with <name>: <Type>.

# Show a user
GET     /users/:username  controllers.Users.showUser(username: String)
# Show user picture in a certain format, e.g. /users/joe/image/114x114
GET     /users/:username/image/$width<[0-9]+>x$height<[0-9]+>  controllers.Users.userImage(username: String, width: Int, height: Int)
# Delete a user
DELETE  /users/:id        controllers.Users.deleteUser(id: Long)

When you want to them appear as query parameters, simply don’t put them in the path.

# /users/browse?sortBy=lastName&sortDirection=asc
GET     /users/browse     controllers.Users.browse(sortBy: String, sortDirection: String)

Query parameters can have default values, so when they are not provided, that value will be passed to the action.

# Default values
GET  /users/browse controllers.Users.browse(sortBy: String ?= "lastName", sortDirection: String ?= "asc", page: Int ?= 1)
# Now you can call /users/browse or /users/browse?sortBy=firstName&page=10

Parameters can also have fixed values. These are default values that cannot be changed at all and help at defining cleaner URLs.

# Fixed values
GET  /users/:username/profile-image    controllers.Users.userImage(username: String, width: Int = 64, height: Int = 64)

By default, each parameter is of type String. This means you can omit the type declaration when you deal with Strings, although I prefer to keep it explicit.

Naturally, you don’t want to convert each parameter from String to some other time in each action, so other value types are automatically converted by Play. Play 2.0.x supports the Scala numerical types Int and Long, 2.1 adds Double, Float, List (Scala and Java), and java.util.UUID.

As mentioned above, you have to be careful about the evaluation order of routes.

GET /users/:username  controllers.Users.showUser(username)
GET /users/browse     controllers.Users.browseUsers()

What would happen, when a user chooses the name browse? Right, the /users/browse route would not match anymore.

Reverse Routing

Every controller and every template automatically imports the reverse routes. A route maps an URL to an action with its parameters, so a reverse route does the opposite by mapping an action call with its parameters to the URL.

Taking our example from above, routes.Users.browse() would resolve to /users/browse, and routes.Users.showUser("joe") to /users/joe. This parameter binding part is especially interesting because it also ensures that fixed parameters are resolved correctly.

What’s especially great is that the routes are compiled, so whenever you change a controller action’s signature, your controllers and templates will no longer compile. So be sure to always use the reverse routes instead of hard-coded URLs.

You can use the reverse routes outside a controller (e.g. in the GlobalSettings) by using the routes subpackage, e.g. controllers.routes.Users.show(1234) or by importing controllers.routes first and then using routes.Users.show(1234).

Reverse routes are of type play.mvc.Call (Java) or play.api.mvc.Call (Scala) and are handled automatically by Templates and Controllers. If you want to generate URLs yourself, for example when sending a double-opt-in e-mail, use the route’s methods url() or absoluteURL(). The latter takes a boolean parameter that can be used to force https URLs.

JavaScript Reverse Routing

The JavaScript reverse router (JS router for short) is a reverse router that can be used to generate URLs and AJAX calls in JavaScript.

You create a JS router by creating an action that returns Routes.javascriptRouter(). The first parameter is the name of the variable that is made available in JavaScript to access the routes. The second parameter is a varargs list of routes you want to use (this means you don’t have all routes available automatically), in the format of routes.javascript.<Controller>.<action>.

public static Result jsRoutes() {
  response().setContentType("text/javascript");
  return ok(Routes.javascriptRouter("jsRoutes", routes.javascript.Users.delete()));
}

Just as in the normal reverse router, you can call url(), absoluteURL() and websocketURL() on the route. Additionally, there is the ajax() function from jQuery, allowing you to quickly create a safe AJAX call like this:

<script src="@routes.Application.jsRoutes()" type="text/javascript"></script>
jsRoutes.controllers.Users.delete(542312).ajax({
  success: function(data) {
    alert("User deleted!");
  }
});

In Scala, you can configure your JS routes directly in the template thanks to the javascriptRouter view helper. It requires an implicit request header in scope.

Global routing

Play’s router is only one way to map URLs to controller actions. The Global settings object has a method onRouteRequest which is called each time an incoming request should be routed to an action. The default implementation obviously asks the Play router to resolve and call the appropriate action. If you need something more dynamic than the rather static routes file (e.g. generated from the database), this is the place to plug in your own router.

Summary

Congratulations, you have now learned the basics about routes. The next part will explain some advanced uses of routes. I will also set up a GitHub repo with all the shown code in action.

Reference

Play Wiki: Java Routes Play Wiki: Scala Routes

Play Routes - Part 2, Advanced Use Cases

Localized routes

There are different approaches to managing the user’s locale in a multi-language site. If most of the content is only available after log-in, a cookie backed by an entry in the User table should be enough. On your landing page you check for the Accept-Language header or check the IP address. Just make sure that the Google Bot can index all publicly visible pages.

On content-heavy sites, one Google-compatible approach is to define the language in the URL. The first component of the URL should be the language (or, if really needed, both language and country). Using the first part of the URL not only to make it easier to differentiate for the user but also to makes it easier to manage the site in Webmaster Tools.

We have two options here. Either we define a regex, or we hardcode our supported languages. The second approach must be chosen when we optimize for Google (see section below).

# Map to language via RegEx
/$language<[a-z]{2}>/videos/:id   controllers.Video.showVideo(id: Long, language: String)
# Hard-code supported languages
/de/videos/:id   controllers.Video.showVideo(id: Long, language = "de")
/en/videos/:id   controllers.Video.showVideo(id: Long, language = "en")

Further approaches to this have been discussed on Stack Overflow (TODO link), but most of them seem unpractical to me.

Now that the route defines the language,

Java???

Action in Scala

SEO-optimized routes

It’s unclear at this point if you actually get a better Google ranking, but at the very least it improves click-through rates.

As shown above, you can set your parameters with p

/de/hilfe                   controllers.StaticPages.showHelp(language = "de")
/fr/aide                    controllers.StaticPages.showHelp(language = "fr")
# If English is your default or fallback language
/help   controllers.StaticPages.showHelp(language: String)
# If you want to enable new languages without touching each and every route (also avoids compiler warnings)
/$language<[a-z]{2}>/help   controllers.StaticPages.showHelp(language: String)

As mentioned in the first part, the reverse router will make sure that your SEO-optimized routes will resolve correctly, i.e. routes.StaticPages.help(language = "de") will resolve to /hilfe.

A few more words on SEO: when you redirect pages that are relevant to Google from one route to another, (e.g. when a username has changed) don’t use redirect() (or Redirect() in Scala), but permanentlyMoved(). The former action’s HTTP status code is 302, which Google doesn’t like, while the latter’s is Google’s preferred 301.

Binders

Let’s say you don’t just want to localize your routes by language, but also by country if applicable. You could either define two Strings for each route, or rather reuse Play’s Lang class (play.i18n.Lang or play.api.i18n.Lang respectively) which already supports both language and country. Your URL could look like this /en/ or /en_US/..., and your controller action like this public static showVideo(Long videoId, Lang lang) / def showVideo(videoId: Long, lang: Lang).

# Two parameters
GET  /$language<[a-z]{2}>/$country<[A-Z]{2}>/article/:artNo    controllers.Articles.showArticle(artNo: String, language: String, country: String)
# Instance of Lang
GET  /$locale<[a-z]{2}_[A-Z]{2}>/article/:artNo   controllers.Articles.showArticle(artNo: String, locale: Lang)

This example is a bit contrived because in reality, you’d probably deploy this app to multiple top-level domains. On the other hand, a .com domain is still the most powerful tld, and, important for an upstarting business, it allows you to concentrate all link power to one domain.

Since Play cannot know how to bind this parameter to a Lang instance, we have to write a Binder.

Java: Scala:

Finally, we’ll tell Play to use the new Binder in project/Build.scala like this:

val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings(
  routesImport += "extensions.Binders._"
)

Summary

Congratulations, you have now learned a lot about routes. I’ve also set up a GitHub repo (here)[] with all the examples from above in both Java and Scala.

Reference

Play Wiki: Java Routes Play Wiki: Scala Routes

Share post

RSS-Feed

News posts directly in your newsreader.

Subscribe feed

Newsletter