(In progress)

Abstract

This article shows the concise and powerful technique to build server endpoints in Tapir library for Scala employing EitherT monad transformer from Cats Effect.

Endpoints and Server Endpoints in Tapir

Tapir is a modern Scala library for Scala which allows to describe HTTP typesafe endpoints using generic syntax, build server endpoints for higher-kinded type F[_] and interpret server endpoints for popular effect systems such as Cats Effect’s IO, ZIO and Akka (NB and Pekko?).

We could approach the endpoint definition in the following way (all DTOs are just some custom types):

  def root =
    endpoint
      .in("things")
      .errorOut(
        oneOf[HttpErrorDTO](
          oneOfVariant(StatusCode.BadRequest, jsonBody[BadRequestDTO]),
          oneOfVariant(StatusCode.InternalServerError, jsonBody[InternalServerErrorDTO])
        )
      )

  val createThingy =
    root
      .post
      .in(jsonBody[CreateThingyDTO])
      .out(jsonBody[CreateThingyResponseDTO])
      .name("CreateThingy")
      .description("Create new thingy")

Next we’ll need to implement server logic for this endpoint:

  private val createThingySE: ServerEndpoint[Any, F] =
    createThingy.serverLogic(createThingy => {
      ??? // create(createThingy).value
    })

serverLogic from tapir’s Endpoint accepts f: I => F[Either[E, O]], which in our case boils down to f: CreateThingyDTO => F[Either[HttpErrorDTO, CreateThingyResponseDTO]]. The result type F[Either[HttpErrorDTO, CreateThingyResponseDTO]] suggests to employ EitherT in this case.

How to use EitherT in the server logic

There’s quite a number of reasons to use EitherT or not as the primary effect type in the application. However, Cats ecosystem is flexible enough to allow us to flip to EitherT and back as necessary. Personally I find using of EitherT everywhere in the application as an overhead, but if I need to provide some HTTP-facing dependencies to server endpoints, I’d rather wire them in EitherT:

final class CreateThingyServerEndpoints[F[_]: Async](
    clientCreate: CreateThingyClient[F],
    producer: EventsProducer[F, ThingyCreateRequestedKey, ThingyCreateRequested]
) extends CreateThingyEndpoints

then in the code that is actually used in server logic we can simply write

  private val createThingySE: ServerEndpoint[Any, F] =
    createThingy.serverLogic(createThingy => {
      create.value
    })

  private def create(
      createThingyDTO: CreateThingyDTO): EitherT[F, HttpErrorDTO, CreateThingyResponseDTO] =
    for
      response <- clientCreate.createThingy(createThingyDTO)
      ...
    yield CreateThingyResponseDTO(response.id)

It’s obvious that for other dependencies we wouldn’t pull HTTP error and result DTOs into the methods definitions (e.g. for producers of events). In this case we can either create an intermediate entity transforming the effect to EitherT or we can write the transformation right away in the function providing server logic:

  private val createThingySE: ServerEndpoint[Any, F] =
    createThingy.serverLogic(createThingy => {
      create.value
    })

  private def create(
      createThingyDTO: CreateThingyDTO): EitherT[F, HttpErrorDTO, CreateThingyResponseDTO] =
    for
      response <- clientCreate.createThingy(createThingyDTO)
      _        <- EitherT(
                        producer
                        .produce(
                            ThingyCreateRequestedKey(response.id),
                            ThingyCreateRequested(response.body)
                        )
                        .attempt
                        .map {
                            case Right(_) =>
                                ().asRight[HttpErrorDTO]
                            case Left(t) =>
                                InternalServerErrorDTO(msg = s"Failed to produce create event: ${t.getMessage}")
                                    .asLeft[Unit]
                        }
                  )
    yield CreateThingyResponseDTO(response.id)

In this case we leverage Cats syntax for applicativeError and either. Additionally, we can write an extension for this:

TODO

Conclusion

EitherT and Tapir can be easily paired together to implement server endpoints, developer should take care about provisioning of http-facing dependencies with EitherT effect type and write extension to lift F[_] into EitherT scope to adhere custom DTO types.