Baku is a Tapir extension library that allows you to easily isolate your API definitions from server and security logic for cleaner, more maintainable code. This makes it simple to share contracts across microservices without exposing the underlying implementation.
SBT:
libraryDependencies += "io.github.arkida39" %% "baku" % "<version>"Mill:
ivy"io.github.arkida39::baku:<version>"replace
versionwith the version of Baku. Each Baku release has a version in format:<min. tapir version>.<baku version>, for example1.12.4.0is compatible with Tapir1.12.4and all newer patch releases (e.g.,1.12.9), but is not guaranteed to work with the next minor/major version (e.g.,1.13.0).
Define these in a standalone module. This allows you to publish the artifact to a repository so consumers can import your API without the server logic or its heavy dependencies.
case class User(token: String)
case class BarInput(id: Int, name: String)
trait MyContract extends Contract {
val foo: PublicEndpoint[String, Unit, String, Any]
val bar: PublicEndpoint[BarInput, Unit, String, Any]
val baz: SecureEndpoint[String, User, BarInput, Unit, String, Any]
}for more information about what each generic argument does, refer to
Contracttrait.
object MyResource extends MyContract, Resource {
import sttp.tapir.*
override val foo = endpoint.get.in("foo").in(query[String]("name"))
.out(stringBody)
override val bar = endpoint.get.in("bar").in(query[Int]("id"))
.in(query[String]("name")).mapInTo[BarInput].out(stringBody)
override val baz = endpoint.get.in("baz").in(query[Int]("id"))
.in(query[String]("name")).mapInTo[BarInput].out(stringBody)
.securityIn(auth.bearer[String]())
}Define these in a private server module. This allows you to wire up heavy dependencies—like database drivers and security providers—while keeping them completely hidden from the consumers of your API.
object MyService extends MyContract, Service[Identity] {
override val foo = (name: String) => Right(s"[FOO] Hello $name")
override val bar = (input: BarInput) =>
Right(s"[BAR] Name: ${input.name}; Id: ${input.id.toString()}")
override val baz = securityLogic[String, User, Unit](_ =>
Right(User("secrettoken")),
).serverLogic(user =>
bar => Right(s"[BAZ] Name: ${bar.name}; Token: ${user.token}"),
)
}instead of
Identity, you can use your desired wrapper effect, making your API definitions portable across ZIO, Cats Effect, etc.
val myComponent = Component.of[MyContract, Identity](MyResource, MyService)this is a macro that uses experimental reflection features, so you would need to use
@experimentalannotation for it to work.
Now you can use this Component to extract the individual wired endpoints, e.g.:
myComponent.foo // val foo: ServerEndpoint[Any, Identity]{type SECURITY_INPUT = Unit; type PRINCIPAL = Unit; type INPUT = String; type ERROR_OUTPUT = Unit; type OUTPUT = String}Or to get the list of all the wired endpoints (in the order they were declared in your Contract):
myComponent.all // val all: List[ServerEndpoint[Any, Identity]
allretains (and combines) the capabilities (Tapir'sRgeneric argument) of all endpoints declared in theContract, so, for example, if one of your endpoints requires aZioStreamscapability, and the other has anFs2Streams[IO]capability, the type will be as follows:val all: List[ServerEndpoint[ZioStreams & Fs2Streams[IO], Identity].