Routing
Routing handles the HTTP specific details of incoming requests and outgoing responses. The main uses of routes are to:
- match HTTP requests and extract Scala values;
- convert Scala values to an HTTP response; and
- 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:
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 thisRoute
. This is often, but not always, the same asI
.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 three components:
- a Request, which describes a HTTP request;
- a Response, which describes a HTTP response; and
- 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:
handle
requires a functionA => B
;handleIO
requires a functionA => IO[B]
; andpassthrough
, which can only be called whenA
is the same type asB
, means that the output of the request is connected directly to the input of the response. This is useful, for example, when the response is loading a static file from the file system and the request produces the name of the file to load.
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:
- constructing a
String
that corresponds to the path matched by theRoute
; - constructing a
String
corresponding to the path and query parameters matched by theRoute
; - 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))
.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
.