The http4s DSL

Recall from earlier that an HttpRoutes[F] is just a type alias for Kleisli[OptionT[F, ?], Request[F], Response[F]]. This provides a minimal foundation for declaring services and executing them on blaze or a servlet container. While this foundation is composable, it is not highly productive. Most service authors will seek a higher level DSL.

Add the http4s-dsl to your build

One option is the http4s-dsl. It is officially supported by the http4s team, but kept separate from core in order to encourage multiple approaches for different needs.

This tutorial assumes that http4s-dsl is on your classpath. Add the following to your build.sbt:

libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-dsl" % http4sVersion,
)

All we need is a REPL to follow along at home:

$ sbt console

The simplest service

We’ll need the following imports to get started:

import cats.effect._
import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._
// Provided by `cats.effect.IOApp`
implicit val timer : Timer[IO] = IO.timer(scala.concurrent.ExecutionContext.global)

The central concept of http4s-dsl is pattern matching. An HttpRoutes[F] is declared as a simple series of case statements. Each case statement attempts to match and optionally extract from an incoming Request[F]. The code associated with the first matching case is used to generate a F[Response[F]].

The simplest case statement matches all requests without extracting anything. The right hand side of the request must return a F[Response[F]].

In the following we use cats.effect.IO as the effect type F.

val service = HttpRoutes.of[IO] {
  case _ =>
    IO(Response(Status.Ok))
}
// service: org.http4s.HttpRoutes[cats.effect.IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$26251/43153737@43ea62f6)

Testing the Service

One beautiful thing about the HttpRoutes[F] model is that we don’t need a server to test our route. We can construct our own request and experiment directly in the REPL.

scala> val getRoot = Request[IO](Method.GET, Uri.uri("/"))
getRoot: org.http4s.Request[cats.effect.IO] = Request(method=GET, uri=/, headers=Headers())

scala> val io = service.orNotFound.run(getRoot)
io: cats.effect.IO[org.http4s.Response[cats.effect.IO]] = <function1>

Where is our Response[F]? It hasn’t been created yet. We wrapped it in an IO. In a real service, generating a Response[F] is likely to be an asynchronous operation with side effects, such as invoking another web service or querying a database, or maybe both. Operating in a F gives us control over the sequencing of operations and lets us reason about our code like good functional programmers. It is the HttpRoutes[F]’s job to describe the task, and the server’s job to run it.

But here in the REPL, it’s up to us to run it:

scala> val response = io.unsafeRunSync
response: org.http4s.Response[cats.effect.IO] = Response(status=200, headers=Headers())

Cool.

Generating responses

We’ll circle back to more sophisticated pattern matching of requests, but it will be a tedious affair until we learn a more succinct way of generating F[Response]s.

Status codes

http4s-dsl provides a shortcut to create an F[Response] by applying a status code:

scala> val okIo = Ok()
okIo: cats.effect.IO[org.http4s.Response[cats.effect.IO]] = IO(Response(status=200, headers=Headers(Content-Length: 0)))

scala> val ok = okIo.unsafeRunSync
ok: org.http4s.Response[cats.effect.IO] = Response(status=200, headers=Headers(Content-Length: 0))

This simple Ok() expression succinctly says what we mean in a service:

HttpRoutes.of[IO] {
  case _ => Ok()
}.orNotFound.run(getRoot).unsafeRunSync
// res1: org.http4s.Response[cats.effect.IO] = Response(status=200, headers=Headers(Content-Length: 0))

This syntax works for other status codes as well. In our example, we don’t return a body, so a 204 No Content would be a more appropriate response:

HttpRoutes.of[IO] {
  case _ => NoContent()
}.orNotFound.run(getRoot).unsafeRunSync
// res2: org.http4s.Response[cats.effect.IO] = Response(status=204, headers=Headers())

Headers

http4s adds a minimum set of headers depending on the response, e.g:

scala> Ok("Ok response.").unsafeRunSync.headers
res3: org.http4s.Headers = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 12)

Extra headers can be added using putHeaders, for example to specify cache policies:

import org.http4s.headers.`Cache-Control`
import org.http4s.CacheDirective.`no-cache`
import cats.data.NonEmptyList
scala> Ok("Ok response.", `Cache-Control`(NonEmptyList(`no-cache`(), Nil))).unsafeRunSync.headers
res4: org.http4s.Headers = Headers(Content-Type: text/plain; charset=UTF-8, Cache-Control: no-cache, Content-Length: 12)

http4s defines all the well known headers directly, but sometimes you need to define custom headers, typically prefixed by an X-. In simple cases you can construct a Header instance by hand:

scala> Ok("Ok response.", Header("X-Auth-Token", "value")).unsafeRunSync.headers
res5: org.http4s.Headers = Headers(Content-Type: text/plain; charset=UTF-8, X-Auth-Token: value, Content-Length: 12)

Cookies

http4s has special support for Cookie headers using the Cookie type to add and invalidate cookies. Adding a cookie will generate the correct Set-Cookie header:

scala> Ok("Ok response.").map(_.addCookie(ResponseCookie("foo", "bar"))).unsafeRunSync.headers
res6: org.http4s.Headers = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 12, Set-Cookie: foo=bar)

Cookie can be further customized to set, e.g., expiration, the secure flag, httpOnly, flag, etc

scala> val cookieResp = {
     |   for {
     |     resp <- Ok("Ok response.")
     |     now <- HttpDate.current[IO]
     |   } yield resp.addCookie(ResponseCookie("foo", "bar", expires = Some(now), httpOnly = true, secure = true))
     | }
cookieResp: cats.effect.IO[org.http4s.Response[cats.effect.IO]] = IO$1332771035

scala> cookieResp.unsafeRunSync.headers
res7: org.http4s.Headers = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 12, Set-Cookie: foo=bar; Expires=Tue, 28 Apr 2020 18:06:02 GMT; Secure; HttpOnly)

To request a cookie to be removed on the client, you need to set the cookie value to empty. http4s can do that with removeCookie:

scala> Ok("Ok response.").map(_.removeCookie("foo")).unsafeRunSync.headers
res8: org.http4s.Headers = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 12, Set-Cookie: foo=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0)

Responding with a body

Simple bodies

Most status codes take an argument as a body. In http4s, Request[F] and Response[F] bodies are represented as a fs2.Stream[F, Byte]. It’s also considered good HTTP manners to provide a Content-Type and, where known in advance, Content-Length header in one’s responses.

All of this hassle is neatly handled by http4s’ EntityEncoders. We’ll cover these in more depth in another tut. The important point for now is that a response body can be generated for any type with an implicit EntityEncoder in scope. http4s provides several out of the box:

scala> Ok("Received request.").unsafeRunSync
res9: org.http4s.Response[cats.effect.IO] = Response(status=200, headers=Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 17))

scala> import java.nio.charset.StandardCharsets.UTF_8
import java.nio.charset.StandardCharsets.UTF_8

scala> Ok("binary".getBytes(UTF_8)).unsafeRunSync
res10: org.http4s.Response[cats.effect.IO] = Response(status=200, headers=Headers(Content-Type: application/octet-stream, Content-Length: 6))

Per the HTTP specification, some status codes don’t support a body. http4s prevents such nonsense at compile time:

scala> NoContent("does not compile")
<console>:30: error: type mismatch;
 found   : String("does not compile")
 required: org.http4s.Header
       NoContent("does not compile")
                 ^

Asynchronous responses

While http4s prefers F[_]: Effect, you may be working with libraries that use standard library Futures. Some relevant imports:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

You can respond with a Future of any type that has an EntityEncoder by lifting it into IO or any F[_] that suspends future. Note: unlike IO, wrapping a side effect in Future does not suspend it, and the resulting expression would still be side effectful, unless we wrap it in IO:

scala> val io = Ok(IO.fromFuture(IO(Future {
     |   println("I run when the future is constructed.")
     |   "Greetings from the future!"
     | })))
io: cats.effect.IO[org.http4s.Response[cats.effect.IO]] = IO(Response(status=200, headers=Headers(Content-Type: text/plain; charset=UTF-8)))

scala> io.unsafeRunSync
res12: org.http4s.Response[cats.effect.IO] = Response(status=200, headers=Headers(Content-Type: text/plain; charset=UTF-8))

As good functional programmers who like to delay our side effects, we of course prefer to operate in Fs:

scala> val io = Ok(IO {
     |   println("I run when the IO is run.")
     |   "Mission accomplished!"
     | })
io: cats.effect.IO[org.http4s.Response[cats.effect.IO]] = IO(Response(status=200, headers=Headers(Content-Type: text/plain; charset=UTF-8)))

scala> io.unsafeRunSync
res13: org.http4s.Response[cats.effect.IO] = Response(status=200, headers=Headers(Content-Type: text/plain; charset=UTF-8))

Note that in both cases, a Content-Length header is calculated. http4s waits for the Future or F to complete before wrapping it in its HTTP envelope, and thus has what it needs to calculate a Content-Length.

Streaming bodies

Streaming bodies are supported by returning a fs2.Stream. Like IO, the stream may be of any type that has an EntityEncoder.

An intro to Stream is out of scope, but we can glimpse the power here. This stream emits the elapsed time every 100 milliseconds for one second:

import fs2.Stream
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
val drip: Stream[IO, String] =
  Stream.awakeEvery[IO](100.millis).map(_.toString).take(10)
// drip: fs2.Stream[cats.effect.IO,String] = Stream(..)

We can see it for ourselves in the REPL:

scala> val dripOutIO = drip.through(fs2.text.lines).through(_.evalMap(s => {IO{println(s); s}})).compile.drain
dripOutIO: cats.effect.IO[Unit] = <function1>

scala> dripOutIO.unsafeRunSync
132454843 nanoseconds232665206 nanoseconds333336774 nanoseconds434382044 nanoseconds535055312 nanoseconds635623678 nanoseconds736417347 nanoseconds837197315 nanoseconds937888983 nanoseconds1038655951 nanoseconds

When wrapped in a Response[F], http4s will flush each chunk of a Stream as they are emitted. Note that a stream’s length can’t generally be anticipated before it runs, so this triggers chunked transfer encoding:

scala> Ok(drip)
res15: cats.effect.IO[org.http4s.Response[cats.effect.IO]] = IO(Response(status=200, headers=Headers(Content-Type: text/plain; charset=UTF-8, Transfer-Encoding: chunked)))

Matching and extracting requests

A Request is a regular case class - you can destructure it to extract its values. By extension, you can also match/case it with different possible destructurings. To build these different extractors, you can make use of the DSL.

The -> object

More often, you extract the Request into a HTTP Method and path info via the -> object. On the left side is the method, and on the right side, the path info. The following matches a request to GET /hello:

scala> HttpRoutes.of[IO] {
     |   case GET -> Root / "hello" => Ok("hello")
     | }
res16: org.http4s.HttpRoutes[cats.effect.IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$26251/43153737@859474)

Path info

Path matching is done on the request’s pathInfo. Path info is the request’s URI’s path after the following:

  • the mount point of the service
  • the prefix, if the service is composed with a Router
  • the prefix, if the service is rewritten with TranslateUri

Matching on request.pathInfo instead of request.uri.path allows multiple services to be composed without rewriting all the path matchers.

Matching paths

A request to the root of the service is matched with the Root extractor. Root consumes the leading slash of the path info. The following matches requests to GET /:

scala> HttpRoutes.of[IO] {
     |   case GET -> Root => Ok("root")
     | }
res17: org.http4s.HttpRoutes[cats.effect.IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$26251/43153737@3235c31f)

We usually match paths in a left-associative manner with Root and /. Each "/" after the initial slash delimits a path segment, and is represented in the DSL with the ‘/’ extractor. Segments can be matched as literals or made available through standard Scala pattern matching. For example, the following service responds with “Hello, Alice!” to GET /hello/Alice:

scala> HttpRoutes.of[IO] {
     |   case GET -> Root / "hello" / name => Ok(s"Hello, $name!")
     | }
res18: org.http4s.HttpRoutes[cats.effect.IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$26251/43153737@53fe8874)

The above assumes only one path segment after "hello", and would not match GET /hello/Alice/Bob. To match to an arbitrary depth, we need a right-associative /: extractor. In this case, there is no Root, and the final pattern is a Path of the remaining segments. This would say "Hello, Alice and Bob!"

scala> HttpRoutes.of[IO] {
     |   case GET -> "hello" /: rest => Ok(s"""Hello, ${rest.toList.mkString(" and ")}!""")
     | }
res19: org.http4s.HttpRoutes[cats.effect.IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$26251/43153737@8735766)

To match a file extension on a segment, use the ~ extractor:

scala> HttpRoutes.of[IO] {
     |   case GET -> Root / file ~ "json" => Ok(s"""{"response": "You asked for $file"}""")
     | }
res20: org.http4s.HttpRoutes[cats.effect.IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$26251/43153737@794740a4)

Handling path parameters

Path params can be extracted and converted to a specific type but are Strings by default. There are numeric extractors provided in the form of IntVar and LongVar, as well as UUIDVar extractor for java.util.UUID.

def getUserName(userId: Int): IO[String] = ???
// getUserName: (userId: Int)cats.effect.IO[String]

val usersService = HttpRoutes.of[IO] {
  case GET -> Root / "users" / IntVar(userId) =>
    Ok(getUserName(userId))
}
// usersService: org.http4s.HttpRoutes[cats.effect.IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$26251/43153737@8969fda)

If you want to extract a variable of type T, you can provide a custom extractor object which implements def unapply(str: String): Option[T], similar to the way in which IntVar does it.

import java.time.LocalDate
import scala.util.Try
import org.http4s.client.dsl.io._
object LocalDateVar {
  def unapply(str: String): Option[LocalDate] = {
    if (!str.isEmpty)
      Try(LocalDate.parse(str)).toOption
    else
      None
  }
}
// defined object LocalDateVar

def getTemperatureForecast(date: LocalDate): IO[Double] = IO(42.23)
// getTemperatureForecast: (date: java.time.LocalDate)cats.effect.IO[Double]

val dailyWeatherService = HttpRoutes.of[IO] {
  case GET -> Root / "weather" / "temperature" / LocalDateVar(localDate) =>
    Ok(getTemperatureForecast(localDate).map(s"The temperature on $localDate will be: " + _))
}
// dailyWeatherService: org.http4s.HttpRoutes[cats.effect.IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$26251/43153737@34239893)

println(GET(Uri.uri("/weather/temperature/2016-11-05")).flatMap(dailyWeatherService.orNotFound(_)).unsafeRunSync)
// Response(status=200, headers=Headers(Content-Type: text/plain; charset=UTF-8))

Handling query parameters

A query parameter needs to have a QueryParamDecoderMatcher provided to extract it. In order for the QueryParamDecoderMatcher to work there needs to be an implicit QueryParamDecoder[T] in scope. QueryParamDecoders for simple types can be found in the QueryParamDecoder object. There are also QueryParamDecoderMatchers available which can be used to return optional or validated parameter values.

In the example below we’re finding query params named country and year and then parsing them as a String and java.time.Year.

import java.time.Year
import cats.data.ValidatedNel
object CountryQueryParamMatcher extends QueryParamDecoderMatcher[String]("country")
// defined object CountryQueryParamMatcher

implicit val yearQueryParamDecoder: QueryParamDecoder[Year] =
  QueryParamDecoder[Int].map(Year.of)
// yearQueryParamDecoder: org.http4s.QueryParamDecoder[java.time.Year] = org.http4s.QueryParamDecoder$$anon$7@704b60a9

object YearQueryParamMatcher extends QueryParamDecoderMatcher[Year]("year")
// defined object YearQueryParamMatcher

def getAverageTemperatureForCountryAndYear(country: String, year: Year): IO[Double] = ???
// getAverageTemperatureForCountryAndYear: (country: String, year: java.time.Year)cats.effect.IO[Double]

val averageTemperatureService = HttpRoutes.of[IO] {
  case GET -> Root / "weather" / "temperature" :? CountryQueryParamMatcher(country) +& YearQueryParamMatcher(year)  =>
    Ok(getAverageTemperatureForCountryAndYear(country, year).map(s"Average temperature for $country in $year was: " + _))
}
// averageTemperatureService: org.http4s.HttpRoutes[cats.effect.IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$26251/43153737@6a0682df)

To support a QueryParamDecoderMatcher[Instant], consider QueryParamCodec#instantQueryParamCodec. That outputs a QueryParamCodec[Instant], which offers both a QueryParamEncoder[Instant] and QueryParamDecoder[Instant].

import java.time.Instant
import java.time.format.DateTimeFormatter
implicit val isoInstantCodec: QueryParamCodec[Instant] =
  QueryParamCodec.instantQueryParamCodec(DateTimeFormatter.ISO_INSTANT)
// isoInstantCodec: org.http4s.QueryParamCodec[java.time.Instant] = org.http4s.QueryParamCodec$$anon$3@53f7b017

object IsoInstantParamMatcher extends QueryParamDecoderMatcher[Instant]("timestamp")
// defined object IsoInstantParamMatcher

Optional query parameters

To accept a optional query parameter a OptionalQueryParamDecoderMatcher can be used.

import java.time.Year
import org.http4s.client.dsl.io._
implicit val yearQueryParamDecoder: QueryParamDecoder[Year] =
  QueryParamDecoder[Int].map(Year.of)
// yearQueryParamDecoder: org.http4s.QueryParamDecoder[java.time.Year] = org.http4s.QueryParamDecoder$$anon$7@7a1f08b3

object OptionalYearQueryParamMatcher extends OptionalQueryParamDecoderMatcher[Year]("year")
// defined object OptionalYearQueryParamMatcher

def getAverageTemperatureForCurrentYear: IO[String] = ???
// getAverageTemperatureForCurrentYear: cats.effect.IO[String]

def getAverageTemperatureForYear(y: Year): IO[String] = ???
// getAverageTemperatureForYear: (y: java.time.Year)cats.effect.IO[String]

val routes2 = HttpRoutes.of[IO] {
  case GET -> Root / "temperature" :? OptionalYearQueryParamMatcher(maybeYear) =>
    maybeYear match {
      case None =>
        Ok(getAverageTemperatureForCurrentYear)
      case Some(year) =>
        Ok(getAverageTemperatureForYear(year))
    }
}
// routes2: org.http4s.HttpRoutes[cats.effect.IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$26251/43153737@74e6c3f4)

Missing required query parameters

A request with a missing required query parameter will fall through to the following case statements and may eventually return a 404. To provide contextual error handling, optional query parameters or fallback routes can be used.

Invalid query parameter handling

To validate query parsing you can use ValidatingQueryParamDecoderMatcher which returns a ParseFailure if the parameter cannot be decoded. Be careful not to return the raw invalid value in a BadRequest because it could be used for Cross Site Scripting attacks.

implicit val yearQueryParamDecoder: QueryParamDecoder[Year] =
  QueryParamDecoder[Int].map(Year.of)
// yearQueryParamDecoder: org.http4s.QueryParamDecoder[java.time.Year] = org.http4s.QueryParamDecoder$$anon$7@38823bd8

object YearQueryParamMatcher extends ValidatingQueryParamDecoderMatcher[Year]("year")
// defined object YearQueryParamMatcher

val routes = HttpRoutes.of[IO] {
  case GET -> Root / "temperature" :? YearQueryParamMatcher(yearValidated) =>
    yearValidated.fold(
      parseFailures => BadRequest("unable to parse argument year"),
      year => Ok(getAverageTemperatureForYear(year))
    )
}
// routes: org.http4s.HttpRoutes[cats.effect.IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$26251/43153737@16818cde)