How better organize tapir endpoints?

613 views Asked by At

I am developing a scala web application with http4s and use tapir for endpoints. I am new in it, and now I am looking for a better way to organize my project.

Now I have different classes with endpoints description and server logic in one. They have a java-spring-like name controller. For example:

class SomeController[F[_] : MonadThrow] {
  val something: ServerEndpoint[Any, F] =
    endpoint
      .description("Something")
      .post
      .in(query[String]("something"))
      .out(jsonBody[String])
      .errorOut(stringBody)
      .serverLogicSuccess {
        something => Monad[F].pure(something)
      }

  val allEndpoints: List[ServerEndpoint[Fs2Streams[F], F]] = List(resend)
}

And then collect them in one configuration, generate open api documentation and http routes. Configuration looks like this:

object RoutesConfiguration {
  private val endpoints: List[ServerEndpoint[Fs2Streams[IO], IO]] = new SomeController[IO].allEndpoints

  private val openApi: List[ServerEndpoint[Any, IO]] =
    SwaggerInterpreter()
      .fromEndpoints(endpoints.map(_.endpoint), "Something", "1.0")

  val routes: HttpRoutes[IO] = Http4sServerInterpreter[IO]().toRoutes(List(openApi, endpoints).flatten)
}

Is it better to separate endpoints description and server logic? Are there better ways to organize endpoints?

2

There are 2 answers

0
Mateusz Kubuszok On

You can separate the logic from the routes e.g. this way:

trait SomethingController[F[_]] {

  def something(sth: String): F[String]
}

class SomethingEndpointsF[_] : MonadThrow](
  controller: SomethingController[F]
) {
  private val something: ServerEndpoint[Any, F] =
    endpoint
      .description("Something")
      .post
      .in(query[String]("something"))
      .out(jsonBody[String])
      .errorOut(stringBody)
      .serverLogicSuccess(controller.something)

  val endpoints: List[ServerEndpoint[Fs2Streams[F], F]] = List(something)
}

Then you could combine several endpoints, interpret them twice: once for HttpRoutes and once for Swagger. Endpoints would be concerned only about defining Tapir logic and have no idea how you want to implement the business logic because you would pass it through the constructor.

0
Gaël J On

One big value of Tapir is that the endpoints are "regular" Scala values. You can (and should) extract small parts of your endpoints and later combine them.

Where your put the cursor depends on how you will reuse some parts or not. One rule though is to separate the endpoints description from the server logic as you don't need the server logic to generate clients or OpenAPI specs.

For instance, you could imagine something like that:

val somethingInput = query[String]("something")

val somethingOutput = ???

val somethingErrorOutput = ???

val somethingEndpoint: PublicEndpoint =
    endpoint
      .description("Something")
      .post
      .in(somethingInput)
      .out(somethingOutput)
      .errorOut(somethingErrorOutput)

Not for Http4s but you might want to look at this sample of code: https://github.com/gaeljw/tapir-play-sample. (Disclaimer: I'm the author).