Skip to content
This repository has been archived by the owner on Jan 24, 2021. It is now read-only.

Collection of what I consider good practices in Http4s (WIP)

Notifications You must be signed in to change notification settings

gvolpe/http4s-good-practices

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 

Repository files navigation

Http4s Good Practices

This is a collection of what I consider good practices that I've been learning along the way, designing and writing APIs using Http4s. Be aware that it could be bias towards my preferences.

Stream App

It is recommended to start the Http Server by extending the given fs2.StreamApp. It'll handle resources cleanup automatically for you. Example:

class HttpServer[F[_]: Effect] extends StreamApp[F] {

  override def stream(args: List[String], requestShutdown: F[Unit]): Stream[F, ExitCode] =
    for {
      exitCode <- BlazeBuilder[F]
                    .bindHttp(8080, "0.0.0.0")
                    .mountService(httpServices)
                    .serve
    } yield exitCode

}

Also notice that I chose to abstract over the effect. This gives you the flexibility to choose your effect implementation only once in exactly one place. For example:

import monix.eval.Task

object Server extends HttpServer[Task]

Or

import cats.effect.IO

object Server extends HttpServer[IO]

Usage of Http Client

Whenever any of your services or HTTP endpoints need to make use of an HTTP Client, make sure that you create only one instance and pass it along.

Given the following service:

class MyService[F[_]: Sync](client: Client[F]) {

  val retrieveSomeData: Stream[F, A] = {
    val request = ???
    client.streaming[Byte](request)(_.body)
  }

}

You'll create the client where you start the HTTP server using the Http1Client.stream (uses Stream.bracket under the hood):

for {
  client   <- Http1Client.stream[F]())
  service  = new MyService[F](client)
  exitCode <- BlazeBuilder[F]
                .bindHttp(8080, "0.0.0.0")
                .serve
} yield exitCode

The same applies to the usage of any of the Fs2 data structures such as Topic, Queue and Promise. Create it on startup and pass it along wherever needed.

HTTP Services Composition

HttpService[F[_]] is an alias for Kleisli[OptionT[F, ?], Request[F], Response[F]] so it is just a function that you can compose. Here's where cats.SemigroupK comes in handy.

Given the following http services, you can combine them into one HttpService using the <+> operator from SemigroupK.

val oneHttpService: HttpService[F] = ???
val twoHttpService: HttpService[F] = ???
val threeHttpService: HttpService[F] = ???

val httpServices: HttpService[F] = (
  oneHttpService <+> twoHttpService <+> threeHttpService
)

NOTE: Don't combine plain HttpService with AuthedService. Use mountService from Server Builder instead for the latter to avoid conflicts since the AuthedService protects an entire namespace and not just an endpoint.

NOTE 2: Since Http4s 0.18.1 you can use AuthMiddleware.withFallThrough(authUser) allowing you to combine plain services with authenticated services.

HTTP Middleware Composition

HttpMiddleware[F[_]] is also a plain function. Basically an alias for HttpService[F] => HttpService[F]. So you can compose it.

def middleware: HttpMiddleware[F] = {
  {(service: HttpService[F]) => GZip(service)(F)} compose
    { service => AutoSlash(service)(F) }
}

Fs2 Scheduler

It's a very common practice to have an fs2.Scheduler as an implicit parameter that many of your services might use to manage time sensible processes. So it makes sense to create it at the the server startup time:

override def stream(args: List[String], requestShutdown: F[Unit]): Stream[F, ExitCode] =
  Scheduler(corePoolSize = 2).flatMap { implicit scheduler =>
    for {
      exitCode <- BlazeBuilder[F]
                    .bindHttp(8080, "0.0.0.0")
                    .serve
    } yield exitCode
  }

Encoders / Decoders

Http4s exposes two interfaces to encode and decode data, namely EntityDecoder[F, A] and EntityEncoder[F, A]. And most of the time Json is the data type you're going to be working with. Here's where Circe shines and integrates very well.

Circe

You need 3 extra dependencies: circe-core, circe-generic and http4s-circe. One option is to define the json codecs in the package object where you define all your HttpServices. This is my setup with a workaround (see http4s/http4s#1648):

import cats.effect.Sync
import io.circe.{Decoder, Encoder}
import org.http4s.{EntityDecoder, EntityEncoder}
import org.http4s.circe.{jsonEncoderOf, jsonOf}

package object http {
  implicit def jsonDecoder[F[_]: Sync, A <: Product: Decoder]: EntityDecoder[F, A] = jsonOf[F, A]
  implicit def jsonEncoder[F[_]: Sync, A <: Product: Encoder]: EntityEncoder[F, A] = jsonEncoderOf[F, A]
}

And then you can use the case class auto derivation feature by just importing io.circe.generic.auto._ in your HttpServices.

If you also want to support value classes out of the box, these two codecs will be helpful (you need an extra dependency circe-generic-extras):

import io.circe.generic.extras.decoding.UnwrappedDecoder
import io.circe.generic.extras.encoding.UnwrappedEncoder

implicit def valueClassEncoder[A: UnwrappedEncoder]: Encoder[A] = implicitly
implicit def valueClassDecoder[A: UnwrappedDecoder]: Decoder[A] = implicitly

Streaming Json Parsers

Error Handling

  • MonadError -> F[A]
  • Either -> F[Error Either A]
  • Both Either and MonadError (adapt to Throwable)

Authentication

  • AuthedService[T, F[_]]
  • AuthedMiddleware[F[_], T]
  • AuthedRequest[F[_], T]

About

Collection of what I consider good practices in Http4s (WIP)

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published