Routes
Routes
models a collection of routes, each of which is defined by a pattern and a handler. This data type can be thought of as modeling a routing table, which decides where to direct every endpoint in an API based on both method and path of the request.
Let's see an example of a simple Routes
that has two routes:
import zio.http._
Routes(
Method.GET / "hello" -> Handler.text("hello"),
Method.GET / "health-check" -> Handler.ok,
)
Building Routes
To build empty routes we have Routes.empty
constructor:
import zio.http._
val routes1 = Routes.empty
We can build routes with the Routes.apply
constructor, which takes varargs of individual Route
values:
object Routes {
def apply[Env, Err](
route : Route[Env, Err],
routes: Route[Env, Err]*,
): Routes[Env, Err] = ???
}
Example:
Routes(
Method.GET / "hello" -> Handler.text("hello"),
Method.GET / "health-check" -> Handler.ok,
Method.POST / "echo" ->
handler { req: Request =>
req.body.asString.map(Response.text(_))
}.sandbox,
)
Please note that in this example, we have used the DSL for constructing routes, which consists of two parts, the route pattern and the handler:
RoutePattern
- The route pattern is responsible for matching the method and path of the incoming request.Handler
- The handler is responsible for producing a response to the matched request.
Although we can build Route
values by using the constructors of Route
, we may prefer to use the DSL for constructing routes which starts in Method
.
Using the /
operator of Method
, we can construct route patterns, which can then be bound to handlers, to create a fully formed route:
val routes2 =
Routes(
Method.GET / "hello" -> Handler.ok,
Method.GET / "goodbye" -> Handler.ok
)
Using the Routes.fromIterable
constructor, we can build routes from an iterable of individual routes.
Nested Routes
Routes can be nested, which means that we can have routes that are themselves collections of other routes. This is useful for organizing routes into hierarchical structures, and for sharing common paths accross routes.
Let's see an example of nested routes:
import zio._
import zio.http._
import zio.http.codec.PathCodec._
val routes =
literal("nest1") /
Routes.fromIterable(
Chunk(
Method.GET / "foo" -> Handler.text("foo"),
Method.GET / "bar" -> Handler.text("bar"),
) ++
Chunk(
literal("nest2") / Routes(
Method.GET / "baz" -> Handler.text("baz"),
Method.GET / "qux" -> Handler.text("qux"),
),
literal("nest2") / Routes(
Method.GET / "quux" -> Handler.text("quux"),
Method.GET / "corge" -> Handler.text("corge"),
),
).map(_.routes).flatten,
)
Combining Routes
The only way to combine two routes collections is to concatenate them using the ++
operator:
val routes3 = routes1 ++ routes2
If the routes have any overlap in their route patterns, then those on the left-side will take precedence over those on the right-hand side.
Transforming Routes
Since routes are just a collection of individual routes, we can transform them in all the same ways that we can transform an individual route. We could do this manually, by building new routes from the old collection of routes, but several convenient methods do this:
Through Handler Transformations
Takes a function of type Handler[Env, Response, Request, Response] => Handler[Env1, Response, Request, Response]
and applies it to all routes:
trait Routes[-Env, +Err] {
def transform[Env1](
f: Handler[Env, Response, Request, Response] => Handler[Env1, Response, Request, Response],
): Routes[Env1, Err] = ???
}
Let's add a delay to all routes:
import zio._
import zio.http._
val routes: Routes[Any, Response] = ???
routes.transform[Any] { handle =>
handler { (request: Request) =>
ZIO.sleep(1.second) *> handle(request)
}
}
Through Applying Middlewares
One of the most common ways to transform routes is to apply a middleware to them. A middleware is a function that takes a collection of routes and returns a new collection of routes. To apply a middleware to Routes
we can use the Routes#@@
method:
trait Routes[-Env, +Err] {
def @@[Env1 <: Env](aspect: Middleware[Env1]): Routes[Env1, Err]
}
Let's add a logging middleware to all routes:
import zio._
import zio.http._
val routes: Routes[Any, Response] = ???
val newRoutes = routes @@ HandlerAspect.dropTrailingSlash
To learn more about middlewares, see the Middleware section.
Handling Errors in Routes
Like ZIO
data type, we can handle errors in Routes
. When we handle errors at the Routes
level, we are handling errors that occur in any of the routes within the Routes
data type.
The following methods are available for error handling:
trait Routes[-Env, +Err] {
def handleError(f: Err => Response): Routes[Env, Nothing]
def handleErrorCause(f: Cause[Err] => Response): Routes[Env, Nothing]
def handleErrorCauseZIO(f: Cause[Err] => ZIO[Any, Nothing, Response]): Routes[Env, Nothing]
}
All of these methods are similar to their ZIO
counterparts, i.e. ZIO#catch*
, but they are applied to the routes.
If we need to take into account what request caused the error, we can use the following methods, instead:
trait Routes[-Env, +Err] {
def handleErrorRequest(f: (Err, Request) => Response): Routes[Env, Nothing]
def handleErrorRequestCause(f: (Request, Cause[Err]) => Response): Routes[Env, Nothing]
def handleErrorRequestCauseZIO(f: (Request, Cause[Err]) => ZIO[Any, Nothing, Response]): Routes[Env, Nothing]
}
Running an App
ZIO HTTP server needs Routes[Env, Response]
for running, so routes that have a Response
as the error type.
We can use Server.serve()
method to bootstrap the server with an instance of Routes[Env, Response]
.:
import zio._
import zio.http._
object HelloWorld extends ZIOAppDefault {
val routes: Routes[Any, Response] = Handler.ok.toRoutes
override def run = Server.serve(routes).provide(Server.default)
}