Tapir Type-Safe API Endpoints
Define and serve HTTP API endpoints with Tapir: type-safe inputs, outputs, error handling, and docs.
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.
HTTP Client with sttp
Make HTTP requests with sttp: GET, POST, headers, JSON bodies, and async backends.
Best for: REST API integration
HTTP Client with Axios Interceptors
Pre-configured Axios instance with request/response interceptors for auth headers, logging, and retry logic.
Best for: Consuming third-party APIs
Native HTTP Server
Create a lightweight HTTP server using Node.js built-in http module with routing and JSON responses.
Best for: Lightweight API server without frameworks
Java Built-in HTTP Server
Create a lightweight HTTP server with com.sun.net.httpserver: routing, JSON responses, file serving.
Best for: Lightweight HTTP servers for testing and prototyping