Routing

Routing handles the HTTP specific details of incoming requests and outgoing responses. The main uses of routes are to:

  1. match HTTP requests and extract Scala values;
  2. convert Scala values to an HTTP response; and
  3. reversing a route to create a link to the route or a client that calls the route.

A Route describes how to convert an incoming request into Scala values, and how to turn Scala values in an outgoing response. A Handler is a Route that also includes the code to convert the values parsed from the request into the values that are used to create the response. In other words, a Handler includes business logic. A Handlers is a collection of Handler.

The Route Type

The Route type is fairly complex, though you can ignore this in most uses.

Route[P <: Tuple, Q <: Tuple, I <: Tuple, O <: Tuple, R]

The types have the following meanings:

  • P is the type of values extracted from the request's path by the Path.
  • Q is the type of query parameters extracted by the Path.
  • I is the type of all values extracted from the HTTP request.
  • O is the type of values to construct an HTTP request to this Route. This is often, but not always, the same as I.
  • R is the type of the value to construct an HTTP response.

Most of these types are tuples because they accumulate values extracted from smaller components of the HTTP request. This will become clearer in the examples below.

Constructing A Route

A Route is constructed from two components:

  1. a Request, which describes a HTTP request;
  2. a Response, which describes a HTTP response; and

The idiomatic way to construct a Route is by calling the Route.apply method, passing a Request and Response. Here is a small example.

val route = 
  Route(
    Request.get(Path / "user" / Param.int), 
    Response.ok(Entity.text)
  )

This route tells us two things:

  1. It will match an incoming GET request to a path like /user/1234 and extract the number as an Int.
  2. It convert a String to an OK response. The response will include that String as the response's entity, and it will have a content type text/plain.

To actually use this route we need to add a handler. In this case a handler would be either a function with type Int => String or with type Int => IO[String]. Here's a very simple example using an Int => String handler.

route.handle(userId => s"You asked for user $userId")

Notice that adding a handler produces a value with a different type, a Handler.

For more details see the separate pages for Request, Response and Handler.

Reverse Routing

There are three forms of reverse routing:

  • constructing a String that corresponds to the path matched by the Route;
  • constructing a String corresponding to the path and query parameters matched by the Route;
  • constructing a HTTP request that will be matched by the Route.

Reverse Routing for Paths

Given a Route you can construct a String containing the path to that route using the pathTo method. This can be used, for example, to embed hyperlinks to routes in generated HTML. Here's an example.

We first create a Route.

val viewUser = Route(Request.get(Path / "user" / Param.int), Response.ok(Entity.text))

Now we can call pathTo to construct a path to that route, which we could embed in an HTML form or a hyperlink.

viewUser.pathTo(1234)
// res0: String = "/user/1234"

Note that we pass to pathTo the parameters for the Path component of the route. If the route has no path parameters there is an overload with no parameters. Here's an example with no parameters.

val users = Route(Request.get(Path / "users"), Response.ok(Entity.text))

Now we can call pathTo without any parameters.

users.pathTo
// res1: String = "/users"

If there is more than one parameter we must collect them in a tuple. The route below has two parameters.

val twoParams = Route(Request.get(Path / "user" / Param.int / Param.string), Response.ok(Entity.text))

Notice when we call pathTo we pass a Tuple2.

twoParams.pathTo((1234, "McBoopy"))
// res2: String = "/user/1234/McBoopy"

Reverse Routing for Paths and Queries

You can use the pathAndQueryTo method to construct a String contains both the path and query parameters to a Route.

Here's an example of a Route that extracts elements from both the path and the query parameters.

val searchUsers = Route(
  Request.get(
    Path / "users" / "search" / Param.string :? Query("start", Param.int)
      .and("stop", Param.int)
  ),
  Response.ok(Entity.text)
)
searchUsers.pathAndQueryTo("scala", (1, 10))
// res3: String = "/users/search/scala?start=1&stop=10"

Combining Routes

Two or more routes can be combined using the orElse method, creating Routes.

val routes = viewUser.orElse(users).orElse(twoParams)

A Route or Routes can also be combined with an Application using overloads of the orElse method, which produces an Application.

Copyright © Noel Welsh. Built with 💖