Tapir Server with Cats-Effect and Pekko HTTP (snippet)
At work, we have been using Akka/Pekko HTTP for some servers, but one of the foundations is Cats-Effect, and I wanted to use Tapir for refactoring one of our microservices.
As you may have noticed, I sometimes use this blog as a replacement for GitHub Gist.
Here’s a simple snippet for using Tapir, with business logic driven by Cats-Effect, using Akka/Pekko HTTP as a backend:
#!/usr/bin/env -S scala shebang
//> using scala "3.8.2"
//> using dep com.softwaremill.sttp.tapir::tapir-core:1.13.10
//> using dep com.softwaremill.sttp.tapir::tapir-pekko-http-server:1.13.10
//> using dep org.typelevel::cats-effect:3.6.3
//> using dep org.apache.pekko::pekko-http:1.3.0
//> using dep org.apache.pekko::pekko-actor-typed:1.4.0
import cats.effect.*
import cats.effect.std.Dispatcher
import org.apache.pekko.actor.typed.ActorSystem
import org.apache.pekko.actor.typed.scaladsl.Behaviors
import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.http.scaladsl.server.Route
import scala.concurrent.ExecutionContext
import sttp.tapir.*
import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter
object Main extends IOApp.Simple {
// Endpoint definition (pure tapir, no effect type yet)
val helloWorldEndpoint =
endpoint.get
.in("hello" / "world")
.in(query[String]("name"))
.out(stringBody)
.errorOut(stringBody)
// Business logic described via Cats Effect IO functions
def greetLogic(name: String): IO[Either[String, String]] =
IO.println(s"Saying hello to: $name")
.as(Right(s"Hello, $name!"))
// Bridge: IO logic → Future, as required by the Pekko HTTP interpreter
def helloWorldRoute(using Dispatcher[IO], ExecutionContext): Route =
PekkoHttpServerInterpreter().toRoute(
helloWorldEndpoint.serverLogic(name =>
summon[Dispatcher[IO]].unsafeToFuture(greetLogic(name))
)
)
override def run: IO[Unit] = {
val res =
for {
given Dispatcher[IO] <- Dispatcher.parallel[IO]
given ExecutionContext <- Resource.eval(IO.executionContext)
given ActorSystem[Nothing] <- Resource(IO {
val system = ActorSystem(Behaviors.empty, "tapir-pekko-sample")
val cancel = IO.fromFuture(IO {
val f = system.whenTerminated
system.terminate()
f
})
(system, cancel.void)
})
_ <- Resource(IO {
// Starts server
val bound = Http().newServerAt("localhost", 8383)
.bind(helloWorldRoute)
val cancel =
IO.fromFuture(IO {
bound.flatMap(_.unbind())
});
(bound, cancel.void)
})
} yield ()
res.use { _ =>
for {
_ <- IO.println(
"Server running at http://localhost:8383 — press ENTER to stop"
)
_ <- IO.readLine.void
} yield ()
}
}
}
| Written by Alexandru Nedelcu