Running as an http4s server using ZIO

The tapir-zio module defines type aliases and extension methods which make it more ergonomic to work with ZIO and tapir. Moreover, tapir-zio-http4s-server contains an interpreter useful when exposing the endpoints using the http4s server.

The *-zio modules depend on ZIO 2.x. For ZIO 1.x support, use modules with the *-zio1 suffix.

You’ll need the following dependency for the ZServerEndpoint type alias and helper classes:

"com.softwaremill.sttp.tapir" %% "tapir-zio" % "1.0.0-M6"

or just add the zio-http4s integration which already depends on tapir-zio:

"com.softwaremill.sttp.tapir" %% "tapir-zio-http4s-server" % "1.0.0-M6"

Next, instead of the usual import sttp.tapir._, you should import (or extend the ZTapir trait, see MyTapir):

import sttp.tapir.ztapir._

This brings into scope all of the basic input/output descriptions, which can be used to define an endpoint.

Note

You should have only one of these imports in your source file. Otherwise, you’ll get naming conflicts. The import sttp.tapir.ztapir._ import is meant as a complete replacement of import sttp.tapir._.

Server logic

When defining the business logic for an endpoint, the following methods are available, which replace the standard ones:

  • def zServerLogic[R](logic: I => ZIO[R, E, O]): ZServerEndpoint[R, C] for public endpoints

  • def zServerSecurityLogic[R, U](f: A => ZIO[R, E, U]): ZPartialServerEndpoint[R, A, U, I, E, O, C] for secure endpoints

The first defines complete server logic, while the second allows defining first the security server logic, and then the rest.

Exposing endpoints using the http4s server

To interpret a ZServerEndpoint as an http4s server, use the following interpreter:

import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter

To help with type-inference, you first need to call ZHttp4sServerInterpreter().from() providing:

  • a single server endpoint: def from[I, E, O, C](se: ZServerEndpoint[R, I, E, O, C])

  • multiple server endpoints: def from[C](serverEndpoints: List[ZServerEndpoint[R, _, _, _, C]])

Then, call .toRoutes to obtain the http4s HttpRoutes instance.

Note that the resulting HttpRoutes always requires Clock in the environment.

If you have multiple endpoints with different environmental requirements, the environment must be first widened so that it is uniform across all endpoints, using the .widen method:

import org.http4s.HttpRoutes
import sttp.tapir.ztapir._
import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter
import zio.Clock
import zio.interop.catz._
import zio.RIO

trait Component1
trait Component2
type Service1 = Component1
type Service2 = Component2

val serverEndpoint1: ZServerEndpoint[Service1, Any] = ???                                                            
val serverEndpoint2: ZServerEndpoint[Service2, Any] = ???

type Env = Service1 with Service2
val routes: HttpRoutes[RIO[Env with Clock, *]] =
  ZHttp4sServerInterpreter().from(List(
    serverEndpoint1.widen[Env], 
    serverEndpoint2.widen[Env]
  )).toRoutes // this is where zio-cats interop is needed

Streaming

The http4s interpreter accepts streaming bodies of type zio.stream.Stream[Throwable, Byte], as described by the ZioStreams capability. Both response bodies and request bodies can be streamed. Usage: streamBody(ZioStreams)(schema, format).

The capability can be added to the classpath independently of the interpreter through the "com.softwaremill.sttp.shared" %% "zio" or tapir-zio dependency.

Http4s backends

Http4s integrates with a couple of server backends, the most popular being Blaze and Ember. In the examples and throughout the docs we use Blaze, but other backends can be used as well. This means adding another dependency, such as:

"org.http4s" %% "http4s-blaze-server" % Http4sVersion

Web sockets

The interpreter supports web sockets, with pipes of type zio.stream.Stream[Throwable, REQ] => zio.stream.Stream[Throwable, RESP]. See web sockets for more details.

However, endpoints which use web sockets need to be interpreted using the ZHttp4sServerInterpreter.fromWebSocket method. This can then be added to a server builder using withHttpWebSocketApp, for example:

import sttp.capabilities.WebSockets
import sttp.capabilities.zio.ZioStreams
import sttp.tapir.{CodecFormat, PublicEndpoint}
import sttp.tapir.ztapir._
import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter
import org.http4s.HttpRoutes
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.server.Router
import org.http4s.server.websocket.WebSocketBuilder2
import scala.concurrent.ExecutionContext
import zio.{RIO, ZEnv, ZIO}
import zio.Clock
import zio.interop.catz._
import zio.stream.Stream

implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global

val wsEndpoint: PublicEndpoint[Unit, Unit, Stream[Throwable, String] => Stream[Throwable, String], ZioStreams with WebSockets] =
  endpoint.get.in("count").out(webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain](ZioStreams))
    
val wsRoutes: WebSocketBuilder2[RIO[Clock, *]] => HttpRoutes[RIO[Clock, *]] =
  ZHttp4sServerInterpreter().fromWebSocket(wsEndpoint.zServerLogic(_ => ???)).toRoutes
    
val serve: ZIO[ZEnv, Throwable, Unit] =
  ZIO.runtime[ZEnv].flatMap { implicit runtime => 
    BlazeServerBuilder[RIO[Clock, *]]
      .withExecutionContext(runtime.platform.executor.asEC)
      .bindHttp(8080, "localhost")
      .withHttpWebSocketApp(wsb => Router("/" -> wsRoutes(wsb)).orNotFound)
      .serve
      .compile
      .drain
  }

Server Sent Events

The interpreter supports SSE (Server Sent Events).

For example, to define an endpoint that returns event stream:

import sttp.capabilities.zio.ZioStreams
import sttp.model.sse.ServerSentEvent
import sttp.tapir.server.http4s.ztapir.{ZHttp4sServerInterpreter, serverSentEventsBody}
import sttp.tapir.PublicEndpoint
import sttp.tapir.ztapir._
import org.http4s.HttpRoutes
import zio.{UIO, RIO}
import zio.Clock
import zio.stream.Stream

val sseEndpoint: PublicEndpoint[Unit, Unit, Stream[Throwable, ServerSentEvent], ZioStreams] = 
  endpoint.get.out(serverSentEventsBody)

val routes: HttpRoutes[RIO[Clock, *]] =
  ZHttp4sServerInterpreter()
    .from(sseEndpoint.zServerLogic(_ => UIO(Stream(ServerSentEvent(Some("data"), None, None, None)))))
    .toRoutes

Examples

There’s a couple of examples of using the ZIO integration available.\