scalaadvanced

Tapir Type-Safe API Endpoints

Define and serve HTTP API endpoints with Tapir: type-safe inputs, outputs, error handling, and docs.

scala
import sttp.tapir.*
import sttp.tapir.json.circe.*
import sttp.tapir.server.http4s.Http4sServerInterpreter
import sttp.tapir.swagger.bundle.SwaggerInterpreter
import io.circe.generic.auto.*
import cats.effect.*
import org.http4s.*
import org.http4s.ember.server.EmberServerBuilder
import com.comcast.ip4s.*

// Domain models
case class User(id: Int, name: String, email: String)
case class CreateUser(name: String, email: String)
case class ErrorResponse(code: Int, message: String)

// Define endpoints (pure descriptions, no logic)
object Endpoints:
  val getUser: PublicEndpoint[Int, ErrorResponse, User, Any] =
    endpoint
      .get
      .in("api" / "users" / path[Int]("userId"))
      .out(jsonBody[User])
      .errorOut(jsonBody[ErrorResponse])
      .description("Get user by ID")

  val listUsers: PublicEndpoint[Unit, ErrorResponse, List[User], Any] =
    endpoint
      .get
      .in("api" / "users")
      .out(jsonBody[List[User]])
      .errorOut(jsonBody[ErrorResponse])
      .description("List all users")

  val createUser: PublicEndpoint[CreateUser, ErrorResponse, User, Any] =
    endpoint
      .post
      .in("api" / "users")
      .in(jsonBody[CreateUser])
      .out(jsonBody[User].and(statusCode(sttp.model.StatusCode.Created)))
      .errorOut(jsonBody[ErrorResponse])
      .description("Create a new user")

  val deleteUser: PublicEndpoint[Int, ErrorResponse, Unit, Any] =
    endpoint
      .delete
      .in("api" / "users" / path[Int]("userId"))
      .errorOut(jsonBody[ErrorResponse])
      .description("Delete user by ID")

  // Endpoint with query parameters
  val searchUsers: PublicEndpoint[(String, Option[Int]), ErrorResponse, List[User], Any] =
    endpoint
      .get
      .in("api" / "users" / "search")
      .in(query[String]("q").description("Search query"))
      .in(query[Option[Int]]("limit").description("Max results"))
      .out(jsonBody[List[User]])
      .errorOut(jsonBody[ErrorResponse])
      .description("Search users")

// Server implementation
object UserService:
  private var users = List(
    User(1, "Alice", "alice@test.com"),
    User(2, "Bob", "bob@test.com")
  )
  private var nextId = 3

  def getUser(id: Int): IO[Either[ErrorResponse, User]] =
    IO.pure(
      users.find(_.id == id)
        .toRight(ErrorResponse(404, s"User $id not found"))
    )

  def listUsers: IO[Either[ErrorResponse, List[User]]] =
    IO.pure(Right(users))

  def createUser(req: CreateUser): IO[Either[ErrorResponse, User]] =
    IO {
      val user = User(nextId, req.name, req.email)
      nextId += 1
      users = users :+ user
      Right(user)
    }

  def searchUsers(query: String, limit: Option[Int]): IO[Either[ErrorResponse, List[User]]] =
    IO.pure {
      val results = users.filter(u =>
        u.name.toLowerCase.contains(query.toLowerCase) ||
        u.email.toLowerCase.contains(query.toLowerCase)
      )
      Right(limit.map(results.take).getOrElse(results))
    }

// Wiring endpoints to logic
object Routes:
  val getUserRoute = Endpoints.getUser.serverLogic(UserService.getUser)
  val listUsersRoute = Endpoints.listUsers.serverLogic(_ => UserService.listUsers)
  val createUserRoute = Endpoints.createUser.serverLogic(UserService.createUser)
  val searchRoute = Endpoints.searchUsers.serverLogic(UserService.searchUsers.tupled)

  val all = List(getUserRoute, listUsersRoute, createUserRoute, searchRoute)

  // Auto-generate Swagger docs
  val swagger = SwaggerInterpreter()
    .fromServerEndpoints[IO](all, "User API", "1.0")

  val routes: HttpRoutes[IO] =
    Http4sServerInterpreter[IO]().toRoutes(all ++ swagger)

object Main extends IOApp.Simple:
  def run: IO[Unit] =
    EmberServerBuilder
      .default[IO]
      .withHost(host"0.0.0.0")
      .withPort(port"8080")
      .withHttpApp(Routes.routes.orNotFound)
      .build
      .use(_ => IO.println("Server at http://localhost:8080") *> IO.never)

Sponsored

Deploy Scala APIs on Railway

Use Cases

  • Type-safe HTTP API definitions
  • Auto-generated API documentation
  • Compile-time endpoint validation

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.