Hello, Tapir ZIO & ZIO HTTP

Pramod Shehan
6 min readJan 22, 2023

--

Tapir

  • Declarative, type-safe web endpoints library.
  • With tapir, you can describe HTTP API endpoints as immutable Scala values. Each endpoint can contain a number of input and output parameters.

Why tapir?

  • type-safety: compile-time guarantees, develop-time completions, read-time information
  • declarative: separate the shape of the endpoint (the “what”), from the server logic (the “how”)
  • OpenAPI / Swagger integration: generate documentation from endpoint descriptions
  • observability: leverage the metadata to report rich metrics and tracing information
  • abstraction: re-use common endpoint definitions, as well as individual inputs/outputs
  • library, not a framework: integrates with your stack

Here we are going to use Tapir and ZIO Http.

Swagger integration

  • Here I am using zio-json(jsonCodec) to encode and decode values. Schema is using for schema representation in Swagger OpenAPI documentation.
import sttp.tapir.Schema
import zio.json.{DeriveJsonCodec, JsonCodec}

case class Vaccinations(vaccinationList: List[VaccinationDetails])

object Vaccinations {
implicit val jsonCodec: JsonCodec[Vaccinations] = DeriveJsonCodec.gen
implicit val schema: Schema[Vaccinations] = Schema.derived
}
  • This is the way how to define endpoint using tapir.
private val baseEndpoint = endpoint.in("api").in("v1").in("vaccination")
  • Here we are doing Swagger OpenAPI documentation representation for GET API. Success Response is vaccinationList type and there may be NotFound error also.
private val vaccinationList = jsonBody[Vaccinations]
.example(Vaccinations(List(VaccinationDetails(1, "Moderna", "Russia"))))

private val getVaccinationErrorOut = oneOf[VaccinationError](
oneOfVariant(StatusCode.NotFound, jsonBody[VaccinationError.NotFound]
.description("Vaccination not found.")))

private val getAllVaccinationsEndpoint =
baseEndpoint.get
.out(vaccinationList)
.errorOut(getVaccinationErrorOut)
  • This is the Swagger representation for that above mentioned GET endpoint.
  • These are the success response and the error responses type representation on the Swagger.
  • We can define all the endpoints such as POST, PUT, DELETE and etc.
// PUT Requst representation
private val putVaccinationEndpoint =
baseEndpoint.put
.in(path[Int]("vac_id"))
.in(vaccinationDetails)
.out(vaccinationList)
.errorOut(getVaccinationInputErrorOut)

// POST Request representation
private val postVaccinationEndpoint =
baseEndpoint.post
.in(vaccinationDetails)
.out(vaccinationList)
.errorOut(getVaccinationInputErrorOut)

//DELETE Request representation
private val deleteVaccinationEndpoint =
baseEndpoint.delete
.in(path[Int]("vac_id"))
.errorOut(getVaccinationInputErrorOut)
  • After that we can define OpenAPI documentation interpreter like this.
  private val endpoints = {
val endpoints = List(
getAllVaccinationsEndpoint,
getVaccinationEndpoint,
postVaccinationEndpoint,
putVaccinationEndpoint,
deleteVaccinationEndpoint
)
endpoints.map(_.tags(List("Vaccination Endpoints")))
}

override def httpRoutes: ZIO[Any, Nothing, HttpApp[Any, Throwable]] =
for {
openApi <- ZIO.succeed(OpenAPIDocsInterpreter().toOpenAPI(endpoints, "Vaccination Service", "0.1"))
endPointsHttp <- ZIO.succeed(ZioHttpInterpreter().toHttp(SwaggerUI[Task](openApi.toYaml)))
} yield endPointsHttp
  • This is Swagger documentation for Vaccination API.

ZIO-HTTP & Tapir 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-http-server contains an interpreter useful when exposing the endpoints using the ZIO Http server.

val tapirVersion = "1.0.2"
com.softwaremill.sttp.tapir" %% "tapir-zio-http-server" % tapirVersion
  • This is the way to combine this public endpoint description with a function, which implements the ZIO server-side logic.
private val allRoutes: Http[Any, Throwable, Request, Response] =
ZioHttpInterpreter().toHttp(List(getAllVaccinationsEndpoint.zServerLogic(_ => vaccinationService.getAllVaccination()),
getVaccinationEndpoint.zServerLogic(vacId => vaccinationService.getVaccinationById(vacId)),
postVaccinationEndpoint.zServerLogic(vacDetails => vaccinationService.addVaccination(vacDetails)),
deleteVaccinationEndpoint.zServerLogic(vacId => vaccinationService.deleteVaccination(vacId)),
putVaccinationEndpoint.zServerLogic(param => vaccinationService.updateVaccination(param._1, param._2))))
  • After that we can combine both Swagger API endpoints and server side endpoints.
override def httpRoutes: ZIO[Any, Nothing, HttpApp[Any, Throwable]] =
for {
openApi <- ZIO.succeed(OpenAPIDocsInterpreter().toOpenAPI(endpoints, "Vaccination Service", "0.1"))
routesHttp <- ZIO.succeed(allRoutes)
endPointsHttp <- ZIO.succeed(ZioHttpInterpreter().toHttp(SwaggerUI[Task](openApi.toYaml)))
} yield routesHttp ++ endPointsHttp

ZIO

ZIO is a next-generation framework for building cloud-native applications on the JVM. With a beginner-friendly yet powerful functional core, ZIO lets developers quickly build best-practice applications that are highly scalable, testable, robust, resilient, resource-safe, efficient, and observable.

ZIO Http

ZIO Http is a scala library for building http apps. It is powered by ZIO for writing, highly scalable and performant web applications using scala.

"io.d11" %% "zhttp" % "2.0.0-RC10"

ZLayer

  • Zlayer describes a layer of an application. every layer in an application requires some services as input RIn and produces some services as the output ROut.
  • ZLayers are,
  1. Recipes for Creating Services
  2. An Alternative to Constructors
  3. Composable
  4. Effectful and Resourceful
  5. Asynchronous
  6. Parallelism
  7. Resilient
  • In VaccinationServer, we are using VaccinationService. So we can use ZLayer from Non-resourceful effects.
import com.pramod.vaccination.exception
import com.pramod.vaccination.exception.VaccinationError
import com.pramod.vaccination.model.{VaccinationDetails, Vaccinations}
import com.pramod.vaccination.service.VaccinationService
import zio.*
import sttp.apispec.openapi.circe.yaml.*
import sttp.model.StatusCode
import sttp.tapir.PublicEndpoint
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter
import sttp.tapir.json.zio.*
import sttp.tapir.server.ziohttp.ZioHttpInterpreter
import sttp.tapir.swagger.SwaggerUI
import sttp.tapir.ztapir.*
import zhttp.http.{Http, HttpApp, Request, Response}

trait VaccinationServer {
def httpRoutes: ZIO[Any, Nothing, HttpApp[Any, Throwable]]
}

object VaccinationServer {

lazy val live: ZLayer[VaccinationService, Nothing, VaccinationServer] = ZLayer {
for {
vaccinationService <- ZIO.service[VaccinationService]
} yield VaccinationServerLive(vaccinationService)
}

def httpRoutes: ZIO[VaccinationServer, Nothing, HttpApp[Any, Throwable]] =
ZIO.serviceWithZIO[VaccinationServer](_.httpRoutes)
}
  • Here I am using mutable ListBuffer to create this Vaccination service. We can use DB operations instead of ListBuffer.
  • Using Server, Creates a server from a http app.
import com.pramod.vaccination.config.HttpConfig
import com.pramod.vaccination.routes.VaccinationServer
import zio.ZIO
import zhttp.service.Server

object App {
def server = ZIO.scoped {
for {
config <- ZIO.service[HttpConfig]
httpApp <- VaccinationServer.httpRoutes
start <- Server(httpApp).withBinding(config.host, config.port).make.orDie
_ <- ZIO.logInfo(s"Server started on port: ${start.port}")
_ <- ZIO.never
} yield ()
}
}
  • Here we extend ZIO mainwith Main object. Earlier we used all the ZIO effects are only descriptions. This is the place that all the ZIO descriptions are ready to be run.
  • Here we need to specify all the services that we have used in our program. We are using provide operator to automatically assemble a layer for the ZIO effect, which translates it to another level.
import com.pramod.vaccination.config.HttpConfig
import com.pramod.vaccination.routes.VaccinationServer
import com.pramod.vaccination.service.VaccinationService
import zhttp.service.Server
import zio.{Scope, ZIO, ZIOAppArgs, ZIOAppDefault, ZLayer}
import io.netty.channel.{ChannelFactory, ServerChannel}
import zhttp.service.EventLoopGroup
import zhttp.service.server.ServerChannelFactory

object Main extends ZIOAppDefault {

override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] = {
App.server.provide(VaccinationServer.live,
VaccinationService.live,
HttpServerSettings.default,
HttpConfig.live,
ZLayer.Debug.tree)
}
}
  • Here I am using ZLayer.Debug.tree to print our application dependency graph.
  • According to the below image, HttpConfig and HttpServerSettings are indepenedent layers. But VaccinatinServer layer is depened on the VaccinationService layer.
  • Here we are using that VaccinationService is in different layers, we don’t need to define it in multiple places. We have only provided it once.
  • ZIO will take care of the application dependency graph. It will automatically create like initialize all these services and then resources safe way and it will close them and we can do it in parallel.
  • Here we don’t need to maintain the dependency order. We can provide these in any order.
  • According to the below picture, you can see, ZIO Http is also using ZIO Fibers concurrency model. When executing below mentioned curl commands on the several terminals, we are receiving the response without any blocking query. ZIO is using different Fibers for each and every request.
for ((i=1;i<=100;i++)); do  sleep 0.5;  curl -v --header "Connection: keep-alive" "http://0.0.0.0:8080/api/v1/vaccination"; done
  1. GET- All get vaccination details- http://0.0.0.0:8080/api/v1/vaccination

2. GET — Get Vaccination by Id- http://0.0.0.0:8080/api/v1/vaccination/1

3. GET — Get Vaccination by Id- http://0.0.0.0:8080/api/v1/vaccination/9

4. PUT —Update existing vaccination id- http://0.0.0.0:8080/api/v1/vaccination/1

5. PUT — Update non existing vaccination id and return error- http://0.0.0.0:8080/api/v1/vaccination/5

6. POST — Add non existing vaccination detail- http://0.0.0.0:8080/api/v1/vaccination

7. DELETE — Delete non existing vaccination id and return error response- http://0.0.0.0:8080/api/v1/vaccination/10

8. DELETE — Delete existing vaccination id- http://0.0.0.0:8080/api/v1/vaccination/2

github project https://github.com/pramodShehan5/vaccination-api

References

https://tapir.softwaremill.com/en/latest/

https://zio.dev/ecosystem/community/zio-http/

https://tapir.softwaremill.com/en/latest/server/ziohttp.html#

--

--