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 deals with a single request and response, and a Routes is a collection of Route.

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:

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 three components:

  1. a Request, which describes a HTTP request;
  2. a Response, which describes a HTTP response; and
  3. a handler, which processes the values extracted from the request and produces the value needed by the response.

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

val route = Route(Request.get(Path / "user" / Param.int), Response.ok(Entity.text))
  .handle(userId => s"You asked for the user ${userId.toString}")

This route will match, for example, a GET request to the path /user/1234 and respond with the string "You asked for the user 1234".

Request and Response have separate pages, so here we'll just discuss the handler. There are three ways to create a handler: using handle, handleIO, or passthrough. Assume the request produces a value of type A and the response needs a value of type B. Then these three methods have the following meaning:

Type Transformations for Handlers

If you dig into the types produced by Request you will notice a lot of tuple types are used. Here's an example, showing a Request producing a Tuple2.

val request = Request.get(Path / Param.int / Param.string)
// request: RequestMethodPath[*:[Int, *:[String, EmptyTuple]], EmptyTuple] = RequestMethodPath(
//   method = GET,
//   path = krop.route.Path@169938c6
// )

This Tuple2 arises because we extract two elements from the HTTP request's path: one Int and one String. However, when you come to use a handler with such a request, you can use a normal function with two arguments not a function that accepts a single Tuple2.

Route(request, Response.ok(Entity.text))
  .handle((int, string) => s"${int.toString}: ${string}")

The conversion between tuples and functions is done by given instances of TupleApply, which allows a function (A, B, ..., N) => Z to be applied to a tuple (A, B, ..., N)

Reverse Routing

There are three forms of reverse routing:

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))
  .handle(userId => s"You asked for the user ${userId.toString}")

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)
// res1: 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))
  .handle(() => "Here are the users.")

Now we can call pathTo without any parameters.

users.pathTo
// res2: 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))
  .handle((userId, name) => s"User with id ${userId} and name ${name}.")

Notice when we call pathTo we pass a Tuple2.

twoParams.pathTo((1234, "McBoopy"))
// res3: 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)
).handle((term, start, stop) => s"Searching for users named $term, from page $start to $stop")
searchUsers.pathAndQueryTo("scala", (1, 10))
// res4: 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.

Request→