sttp
Unified HTTP client library for Scala. Provides common interface supporting multiple backends (Java HttpClient, Akka HTTP, http4s, OkHttp, etc.). Seamless integration with JSON processing (circe, uPickle, etc.), streaming (fs2, ZIO Streams, etc.), and functional programming libraries.
GitHub Overview
softwaremill/sttp
The Scala HTTP client you always wanted!
Topics
Star History
Library
sttp
Overview
sttp is "The Scala HTTP client you always wanted!" developed as a next-generation HTTP client library in the Scala ecosystem, designed with emphasis on "unified API design and diverse programming paradigm support." It enables unified API usage across synchronous processing, Future-based, and functional effect systems (cats-effect, ZIO, Monix, etc.). Through immutable request definitions, pluggable backends, type-safe response handling, URI interpolation, and rich ecosystem integration, it enables intuitive and efficient HTTP communication across various Scala programming styles.
Details
sttp 2025 edition (v4.0 series) is a mature Scala HTTP client solution co-developed by SoftwareMill and VirtusLab. The core abstractions of Request[T], Backend[F], and Response[T] clearly separate request description, execution, and response handling. Features include multi-platform design supporting Scala 2.12/2.13/3, JVM (Java 11+)/Scala.JS/Scala Native compatibility, pluggable backends (Java HttpClient, Akka HTTP, Pekko HTTP, http4s, OkHttp, etc.), comprehensive authentication support, streaming integration, and testing support tools. With an API emphasizing developer experience and productivity improvement, it addresses diverse needs of Scala developers.
Key Features
- Unified API Design: Consistent development experience across synchronous, asynchronous, and functional effect systems
- Immutable Request Definition: Type-safe and reusable HTTP request description via Request[T]
- Pluggable Backends: Flexible HTTP implementation switching through Backend[F] abstraction
- Type-safe Response Handling: Powerful type-safe response transformation via ResponseAs[T]
- Multi-platform Support: Unified support for JVM/Scala.JS/Scala Native
- Rich Ecosystem Integration: Tight integration with JSON, streaming, and monitoring libraries
Pros and Cons
Pros
- Highest level of integration and developer experience in the Scala ecosystem
- Natural affinity with functional programming paradigms
- Significant reduction in runtime errors through strong type safety
- High flexibility and customizability through pluggable architecture
- Comprehensive documentation and active community support
- Wide applicability through multi-platform support
Cons
- High learning cost due to Scala-specific concept understanding requirements
- Initial setup complexity due to rich functionality
- Required understanding of functional effect systems
- Limitations in interoperability with Java/other language ecosystems
- May be overly feature-rich for small projects
- Potential increase in compilation time
Reference Pages
Code Examples
Installation and Basic Setup
// build.sbt - Basic configuration
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client4" %% "core" % "4.0.9"
)
// JSON processing integration
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client4" %% "core" % "4.0.9",
"com.softwaremill.sttp.client4" %% "circe" % "4.0.9", // circe integration
"io.circe" %% "circe-generic" % "0.14.7"
)
// ZIO integration
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client4" %% "core" % "4.0.9",
"com.softwaremill.sttp.client4" %% "zio" % "4.0.9",
"dev.zio" %% "zio" % "2.1.6"
)
// cats-effect integration
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client4" %% "core" % "4.0.9",
"com.softwaremill.sttp.client4" %% "cats" % "4.0.9",
"org.typelevel" %% "cats-effect" % "3.5.4"
)
package main
import sttp.client4.*
// Basic client initialization
object SttpBasics {
// Quick start for experimentation and learning
def quickExample(): Unit = {
import sttp.client4.quick.*
// Immediate request with global synchronous backend
val response = quickRequest.get(uri"https://api.example.com/users").send()
println(response.body)
}
// Production synchronous backend
def syncExample(): Unit = {
val backend = DefaultSyncBackend()
val request = basicRequest
.get(uri"https://api.example.com/users")
.header("Accept", "application/json")
.header("User-Agent", "MyScalaApp/1.0")
val response = request.send(backend)
response.body match {
case Right(content) => println(s"Success: $content")
case Left(error) => println(s"Error: $error")
}
backend.close()
}
// Future-based asynchronous backend
def futureExample(): Unit = {
import scala.concurrent.{Future, ExecutionContext}
import scala.util.{Success, Failure}
implicit val ec: ExecutionContext = ExecutionContext.global
val backend = DefaultFutureBackend()
val request = basicRequest.get(uri"https://api.example.com/users")
val responseFuture: Future[Response[Either[String, String]]] = request.send(backend)
responseFuture.onComplete {
case Success(response) =>
println(s"Response: ${response.body}")
case Failure(exception) =>
println(s"Failed: ${exception.getMessage}")
}
backend.close()
}
}
Basic HTTP Requests (GET/POST/PUT/DELETE)
import sttp.client4.*
import io.circe.*
import io.circe.generic.semiauto.*
import sttp.client4.circe.*
// Data class definitions
case class User(id: Int, name: String, email: String, age: Int)
case class UserCreateRequest(name: String, email: String, age: Int)
case class UserUpdateRequest(name: Option[String], email: Option[String], age: Option[Int])
// JSON Codec definitions
implicit val userDecoder: Decoder[User] = deriveDecoder[User]
implicit val userEncoder: Encoder[User] = deriveEncoder[User]
implicit val userCreateEncoder: Encoder[UserCreateRequest] = deriveEncoder[UserCreateRequest]
implicit val userUpdateEncoder: Encoder[UserUpdateRequest] = deriveEncoder[UserUpdateRequest]
object HttpRequestExamples {
val backend = DefaultSyncBackend()
// GET request (basic)
def getUserList(): Either[String, List[User]] = {
val response = basicRequest
.get(uri"https://api.example.com/users")
.response(asJson[List[User]])
.send(backend)
response.body match {
case Right(users) => Right(users)
case Left(error) => Left(s"Failed to get user list: $error")
}
}
// GET request with query parameters
def getUserListWithParams(page: Int, limit: Int, sortBy: String): Either[String, List[User]] = {
val response = basicRequest
.get(uri"https://api.example.com/users?page=$page&limit=$limit&sort=$sortBy")
.response(asJson[List[User]])
.send(backend)
response.body match {
case Right(users) =>
println(s"Retrieved user count: ${users.length}")
Right(users)
case Left(error) => Left(s"Failed to get page: $error")
}
}
// GET request (individual user)
def getUser(userId: Int): Either[String, User] = {
val response = basicRequest
.get(uri"https://api.example.com/users/$userId")
.response(asJson[User])
.send(backend)
response.body match {
case Right(user) => Right(user)
case Left(error) => Left(s"Failed to get user: $error")
}
}
// POST request (create user)
def createUser(userRequest: UserCreateRequest): Either[String, User] = {
val response = basicRequest
.post(uri"https://api.example.com/users")
.body(userRequest)
.response(asJson[User])
.send(backend)
response.body match {
case Right(user) =>
println(s"User created successfully: ID=${user.id}, Name=${user.name}")
Right(user)
case Left(error) => Left(s"Failed to create user: $error")
}
}
// PUT request (update user)
def updateUser(userId: Int, updateRequest: UserUpdateRequest): Either[String, User] = {
val response = basicRequest
.put(uri"https://api.example.com/users/$userId")
.body(updateRequest)
.response(asJson[User])
.send(backend)
response.body match {
case Right(user) =>
println(s"User updated successfully: ${user.name}")
Right(user)
case Left(error) => Left(s"Failed to update user: $error")
}
}
// DELETE request
def deleteUser(userId: Int): Either[String, Unit] = {
val response = basicRequest
.delete(uri"https://api.example.com/users/$userId")
.send(backend)
if (response.code.code == 204) {
println(s"User deleted successfully: ID=$userId")
Right(())
} else {
Left(s"Failed to delete user: ${response.code}")
}
}
// Form data submission
def submitForm(username: String, email: String, category: String): Either[String, String] = {
val response = basicRequest
.post(uri"https://api.example.com/register")
.body(Map(
"username" -> username,
"email" -> email,
"category" -> category
))
.send(backend)
response.body
}
// Usage example
def main(args: Array[String]): Unit = {
// Get user list
getUserListWithParams(1, 10, "created_at") match {
case Right(users) => users.foreach(user => println(s"User: ${user.name}"))
case Left(error) => println(error)
}
// Create user
val newUser = UserCreateRequest("John Doe", "[email protected]", 30)
createUser(newUser) match {
case Right(user) => println(s"Created user: ${user.id}")
case Left(error) => println(error)
}
backend.close()
}
}
Authentication and Security Configuration
import sttp.client4.*
import sttp.client4.wrappers.DigestAuthenticationBackend
object AuthenticationExamples {
val backend = DefaultSyncBackend()
// Basic authentication
def basicAuthExample(): Unit = {
val response = basicRequest
.get(uri"https://api.example.com/protected")
.auth.basic("username", "password")
.send(backend)
println(s"Basic auth response: ${response.body}")
}
// Bearer Token authentication
def bearerTokenExample(): Unit = {
val token = "your-jwt-token-here"
val response = basicRequest
.get(uri"https://api.example.com/user/profile")
.auth.bearer(token)
.send(backend)
println(s"Bearer auth response: ${response.body}")
}
// API Key authentication
def apiKeyExample(): Unit = {
val apiKey = "your-api-key"
val response = basicRequest
.get(uri"https://api.example.com/data")
.header("X-API-Key", apiKey)
.header("X-Client-ID", "your-client-id")
.send(backend)
println(s"API key auth response: ${response.body}")
}
// OAuth 2.0 example
case class OAuthTokenResponse(
access_token: String,
token_type: String,
expires_in: Int,
refresh_token: Option[String]
)
implicit val oauthDecoder: Decoder[OAuthTokenResponse] = deriveDecoder[OAuthTokenResponse]
def oauth2Example(): Unit = {
// Acquire access token
val tokenResponse = basicRequest
.post(uri"https://auth.example.com/oauth2/token")
.auth.basic("client_id", "client_secret")
.body(Map(
"grant_type" -> "client_credentials",
"scope" -> "read write"
))
.response(asJson[OAuthTokenResponse])
.send(backend)
tokenResponse.body match {
case Right(token) =>
println(s"Token acquired successfully: ${token.access_token}")
// API call with acquired token
val apiResponse = basicRequest
.get(uri"https://api.example.com/user/profile")
.auth.bearer(token.access_token)
.send(backend)
println(s"API call result: ${apiResponse.body}")
case Left(error) =>
println(s"Token acquisition failed: $error")
}
}
// Digest authentication
def digestAuthExample(): Unit = {
val digestBackend = DigestAuthenticationBackend(backend)
val response = basicRequest
.get(uri"https://api.example.com/digest-protected")
.auth.digest("username", "password")
.send(digestBackend)
println(s"Digest auth response: ${response.body}")
}
// Custom authentication headers
def customAuthExample(): Unit = {
val timestamp = System.currentTimeMillis().toString
val signature = generateSignature("api-key", "secret", timestamp)
val response = basicRequest
.get(uri"https://api.example.com/secure")
.header("X-API-Key", "your-api-key")
.header("X-Timestamp", timestamp)
.header("X-Signature", signature)
.send(backend)
println(s"Custom auth response: ${response.body}")
}
def generateSignature(apiKey: String, secret: String, timestamp: String): String = {
// Actual signature generation logic
import java.security.MessageDigest
val input = s"$apiKey$timestamp$secret"
MessageDigest.getInstance("SHA-256").digest(input.getBytes).map("%02x".format(_)).mkString
}
}
ZIO Integration and Functional Effects
import sttp.client4.*
import sttp.client4.httpclient.zio.HttpClientZioBackend
import zio.*
import io.circe.*
import io.circe.generic.semiauto.*
import sttp.client4.circe.*
object ZIOIntegrationExample extends ZIOAppDefault {
case class User(id: Int, name: String, email: String)
case class ApiError(code: Int, message: String)
implicit val userDecoder: Decoder[User] = deriveDecoder[User]
implicit val errorDecoder: Decoder[ApiError] = deriveDecoder[ApiError]
// HTTP client service using ZIO
trait UserService {
def getUser(id: Int): Task[User]
def createUser(name: String, email: String): Task[User]
def getAllUsers(): Task[List[User]]
}
case class UserServiceImpl(backend: SttpBackend[Task, Any]) extends UserService {
def getUser(id: Int): Task[User] = {
basicRequest
.get(uri"https://api.example.com/users/$id")
.response(asJson[User])
.send(backend)
.flatMap { response =>
response.body match {
case Right(user) => ZIO.succeed(user)
case Left(error) => ZIO.fail(new RuntimeException(s"Failed to get user: $error"))
}
}
}
def createUser(name: String, email: String): Task[User] = {
val userData = Map("name" -> name, "email" -> email)
basicRequest
.post(uri"https://api.example.com/users")
.body(userData)
.response(asJson[User])
.send(backend)
.flatMap { response =>
response.body match {
case Right(user) => ZIO.succeed(user)
case Left(error) => ZIO.fail(new RuntimeException(s"Failed to create user: $error"))
}
}
}
def getAllUsers(): Task[List[User]] = {
basicRequest
.get(uri"https://api.example.com/users")
.response(asJson[List[User]])
.send(backend)
.flatMap { response =>
response.body match {
case Right(users) => ZIO.succeed(users)
case Left(error) => ZIO.fail(new RuntimeException(s"Failed to get user list: $error"))
}
}
}
}
// Layer definitions
val userServiceLayer: ZLayer[SttpBackend[Task, Any], Nothing, UserService] =
ZLayer.fromFunction(UserServiceImpl.apply _)
val backendLayer: TaskLayer[SttpBackend[Task, Any]] =
HttpClientZioBackend.layer()
// Main program
def run: ZIO[Any, Throwable, Unit] = {
val program = for {
userService <- ZIO.service[UserService]
// Concurrent user retrieval
users <- ZIO.collectAllPar(List(
userService.getUser(1),
userService.getUser(2),
userService.getUser(3)
)).catchAll { error =>
ZIO.logError(s"User retrieval error: ${error.getMessage}") *>
ZIO.succeed(List.empty[User])
}
_ <- ZIO.log(s"Retrieved user count: ${users.length}")
// Create new user
newUser <- userService.createUser("John Doe", "[email protected]").catchAll { error =>
ZIO.logError(s"User creation error: ${error.getMessage}") *>
ZIO.fail(error)
}
_ <- ZIO.log(s"Created user: ${newUser.name}")
// Get all users
allUsers <- userService.getAllUsers()
_ <- ZIO.log(s"Total user count: ${allUsers.length}")
} yield ()
program.provide(
userServiceLayer,
backendLayer
)
}
}
Error Handling and Retry Functionality
import sttp.client4.*
import sttp.client4.wrappers.{TryBackend, EitherBackend}
import scala.util.{Try, Success, Failure}
import scala.concurrent.duration.*
object ErrorHandlingExamples {
val backend = DefaultSyncBackend()
// Try-based error handling
def tryBasedHandling(): Unit = {
val tryBackend = TryBackend(backend)
val result: Try[Response[Either[String, String]]] = basicRequest
.get(uri"https://api.example.com/unreliable")
.send(tryBackend)
result match {
case Success(response) =>
response.body match {
case Right(content) => println(s"Success: $content")
case Left(error) => println(s"Response error: $error")
}
case Failure(exception) =>
println(s"Network error: ${exception.getMessage}")
}
}
// Either-based error handling
def eitherBasedHandling(): Unit = {
val eitherBackend = EitherBackend(backend)
val result: Either[Exception, Response[Either[String, String]]] = basicRequest
.get(uri"https://api.example.com/data")
.send(eitherBackend)
result match {
case Right(response) =>
println(s"Response retrieved successfully: ${response.code}")
case Left(exception) =>
println(s"Request failed: ${exception.getMessage}")
}
}
// Custom response handling
case class ApiError(code: Int, message: String, details: String)
implicit val apiErrorDecoder: Decoder[ApiError] = deriveDecoder[ApiError]
def customResponseHandling(): Unit = {
val response = basicRequest
.get(uri"https://api.example.com/data")
.response(asEither(asJson[ApiError], asJson[List[User]]))
.send(backend)
response.body match {
case Right(Right(users)) =>
println(s"Data retrieved successfully: ${users.length} items")
case Right(Left(error)) =>
println(s"API error: ${error.message} (code: ${error.code})")
case Left(httpError) =>
println(s"HTTP error: $httpError")
}
}
// Retry mechanism implementation
def retryableRequest[T](
request: Request[T, Any],
maxRetries: Int = 3,
backoffDelay: FiniteDuration = 1.second
): Either[String, Response[T]] = {
def attempt(retriesLeft: Int): Either[String, Response[T]] = {
val response = request.send(backend)
if (response.code.code >= 500 && retriesLeft > 0) {
println(s"Server error (${response.code}), retries left: $retriesLeft")
Thread.sleep(backoffDelay.toMillis)
attempt(retriesLeft - 1)
} else if (response.code.code >= 400) {
Left(s"Client error: ${response.code}")
} else {
Right(response)
}
}
attempt(maxRetries)
}
// Circuit breaker implementation
class CircuitBreaker(threshold: Int, timeout: FiniteDuration) {
private var failureCount = 0
private var lastFailureTime: Long = 0
private var state: String = "closed" // "closed", "open", "half-open"
def execute[T](request: Request[T, Any]): Either[String, Response[T]] = {
state match {
case "open" =>
if (System.currentTimeMillis() - lastFailureTime > timeout.toMillis) {
state = "half-open"
println("Circuit breaker: half-open state")
executeRequest(request)
} else {
Left("Circuit breaker is open")
}
case _ =>
executeRequest(request)
}
}
private def executeRequest[T](request: Request[T, Any]): Either[String, Response[T]] = {
val response = request.send(backend)
if (response.code.code >= 500) {
failureCount += 1
lastFailureTime = System.currentTimeMillis()
if (failureCount >= threshold) {
state = "open"
println(s"Circuit breaker opened (failure count: $failureCount)")
}
Left(s"Server error: ${response.code}")
} else {
// Reset on success
failureCount = 0
state = "closed"
Right(response)
}
}
}
def circuitBreakerExample(): Unit = {
val circuitBreaker = new CircuitBreaker(3, 30.seconds)
for (i <- 1 to 10) {
val result = circuitBreaker.execute(
basicRequest.get(uri"https://api.example.com/unreliable")
)
result match {
case Right(response) =>
println(s"Attempt $i successful: ${response.code}")
case Left(error) =>
println(s"Attempt $i failed: $error")
}
Thread.sleep(1000)
}
}
}
File Operations and Advanced Features
import sttp.client4.*
import java.io.File
import java.nio.file.{Files, Paths}
object FileOperationsExamples {
val backend = DefaultSyncBackend()
// File upload
def uploadFile(): Unit = {
val file = new File("/path/to/document.pdf")
val response = basicRequest
.post(uri"https://api.example.com/upload")
.multipartBody(
multipart("file", file).fileName("document.pdf").contentType("application/pdf"),
multipart("description", "Important document"),
multipart("category", "legal")
)
.header("Authorization", "Bearer your-token")
.send(backend)
response.body match {
case Right(result) => println(s"Upload successful: $result")
case Left(error) => println(s"Upload failed: $error")
}
}
// Multiple file upload
def uploadMultipleFiles(): Unit = {
val document = new File("/path/to/document.pdf")
val image = new File("/path/to/image.jpg")
val data = new File("/path/to/data.json")
val response = basicRequest
.post(uri"https://api.example.com/upload/batch")
.multipartBody(
multipart("title", "Batch upload"),
multipart("description", "Multiple file batch processing"),
multipart("document", document).fileName("doc.pdf"),
multipart("image", image).fileName("image.jpg"),
multipart("data", data).fileName("data.json")
)
.send(backend)
println(s"Batch upload result: ${response.body}")
}
// File download
def downloadFile(): Unit = {
val downloadPath = "/path/to/downloads/downloaded_file.pdf"
val response = basicRequest
.get(uri"https://api.example.com/files/document.pdf")
.response(asFile(new File(downloadPath)))
.send(backend)
response.body match {
case Right(file) =>
println(s"Download completed: ${file.getAbsolutePath}")
println(s"File size: ${file.length()} bytes")
case Left(error) =>
println(s"Download failed: $error")
}
}
// Streaming download (for large files)
def streamingDownload(): Unit = {
val response = basicRequest
.get(uri"https://api.example.com/large-file.zip")
.response(asStream[InputStream])
.send(backend)
response.body match {
case Right(inputStream) =>
val outputPath = Paths.get("/path/to/downloads/large-file.zip")
Files.copy(inputStream, outputPath)
inputStream.close()
println(s"Streaming download completed: $outputPath")
case Left(error) =>
println(s"Streaming download failed: $error")
}
}
// Binary data handling
def binaryDataHandling(): Unit = {
val binaryData: Array[Byte] = Files.readAllBytes(Paths.get("/path/to/binary.dat"))
// Binary data upload
val uploadResponse = basicRequest
.post(uri"https://api.example.com/binary")
.body(binaryData)
.header("Content-Type", "application/octet-stream")
.send(backend)
// Binary data download
val downloadResponse = basicRequest
.get(uri"https://api.example.com/binary/12345")
.response(asByteArray)
.send(backend)
downloadResponse.body match {
case Right(bytes) =>
Files.write(Paths.get("/path/to/received_binary.dat"), bytes)
println(s"Binary data received: ${bytes.length} bytes")
case Left(error) =>
println(s"Binary data reception failed: $error")
}
}
// WebSocket connection example
def webSocketExample(): Unit = {
import sttp.client4.httpclient.HttpClientSyncBackend
import sttp.ws.WebSocket
val wsBackend = HttpClientSyncBackend()
val response = basicRequest
.get(uri"wss://echo.websocket.org")
.response(asWebSocket { ws: WebSocket[Identity] =>
// Send message
ws.sendText("Hello WebSocket!")
// Receive message
val received = ws.receiveText()
println(s"Received message: $received")
// Close WebSocket
ws.close()
received.getOrElse("")
})
.send(wsBackend)
response.body match {
case Right(result) => println(s"WebSocket communication result: $result")
case Left(error) => println(s"WebSocket connection failed: $error")
}
wsBackend.close()
}
}
Performance Optimization and Monitoring
import sttp.client4.*
import sttp.client4.logging.LoggingBackend
import sttp.client4.wrappers.FollowRedirectsBackend
import scala.concurrent.duration.*
object PerformanceOptimizationExamples {
// Optimized backend configuration
def createOptimizedBackend(): SttpBackend[Identity, Any] = {
val baseBackend = DefaultSyncBackend(
options = SttpBackendOptions(
connectionTimeout = 30.seconds,
proxy = None
)
)
// Backend with logging functionality
val loggingBackend = LoggingBackend(
delegate = baseBackend,
logRequestBody = true,
logResponseBody = true
)
// Redirect support
FollowRedirectsBackend(loggingBackend)
}
// Connection pool optimization
def connectionPoolOptimization(): Unit = {
import java.net.http.HttpClient
import java.time.Duration
val httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.version(HttpClient.Version.HTTP_2) // HTTP/2 preferred
.followRedirects(HttpClient.Redirect.NORMAL)
.build()
val backend = HttpClientSyncBackend.usingClient(httpClient)
// Test request for configuration verification
val response = basicRequest
.get(uri"https://httpbin.org/get")
.send(backend)
println(s"HTTP version: ${response.httpVersion}")
println(s"Response time: ${response.responseTime}")
backend.close()
}
// Concurrent request processing
def concurrentRequests(): Unit = {
import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.duration.*
import scala.util.{Success, Failure}
implicit val ec: ExecutionContext = ExecutionContext.global
val backend = DefaultFutureBackend()
val userIds = (1 to 20).toList
// Concurrent user information retrieval
val futures = userIds.map { id =>
basicRequest
.get(uri"https://api.example.com/users/$id")
.response(asJson[User])
.send(backend)
.map { response =>
(id, response.body)
}
}
// Wait for all responses
val startTime = System.currentTimeMillis()
Future.sequence(futures).onComplete {
case Success(results) =>
val endTime = System.currentTimeMillis()
val successCount = results.count(_._2.isRight)
println(s"Concurrent processing completed: ${endTime - startTime}ms")
println(s"Success: $successCount / ${results.length}")
backend.close()
case Failure(exception) =>
println(s"Concurrent processing failed: ${exception.getMessage}")
backend.close()
}
}
// Request monitoring and metrics
def requestMonitoring(): Unit = {
var requestCount = 0
var totalResponseTime = 0L
val monitoringBackend = new SttpBackend[Identity, Any] {
def send[T](request: Request[T, Any]): Response[T] = {
val startTime = System.currentTimeMillis()
requestCount += 1
println(s"Request #$requestCount: ${request.method} ${request.uri}")
val response = DefaultSyncBackend().send(request)
val responseTime = System.currentTimeMillis() - startTime
totalResponseTime += responseTime
println(s"Response #$requestCount: ${response.code} (${responseTime}ms)")
println(s"Average response time: ${totalResponseTime / requestCount}ms")
response
}
def close(): Unit = DefaultSyncBackend().close()
def responseMonad: MonadError[Identity] = IdMonad
}
// Execute requests with monitoring functionality
val response1 = basicRequest.get(uri"https://httpbin.org/get").send(monitoringBackend)
val response2 = basicRequest.get(uri"https://httpbin.org/status/200").send(monitoringBackend)
val response3 = basicRequest.get(uri"https://httpbin.org/delay/1").send(monitoringBackend)
println(s"Total requests: $requestCount")
println(s"Average response time: ${totalResponseTime / requestCount}ms")
monitoringBackend.close()
}
// Caching functionality implementation
class CachingBackend(delegate: SttpBackend[Identity, Any]) extends SttpBackend[Identity, Any] {
private val cache = scala.collection.mutable.Map[String, (Response[String], Long)]()
private val cacheTimeout = 60000 // 1 minute
def send[T](request: Request[T, Any]): Response[T] = {
val cacheKey = s"${request.method}:${request.uri}"
val currentTime = System.currentTimeMillis()
// Only cache GET requests
if (request.method.method == "GET") {
cache.get(cacheKey) match {
case Some((cachedResponse, timestamp)) if currentTime - timestamp < cacheTimeout =>
println(s"Cache hit: $cacheKey")
cachedResponse.asInstanceOf[Response[T]]
case _ =>
println(s"Cache miss: $cacheKey")
val response = delegate.send(request)
if (response.code.code == 200) {
cache.put(cacheKey, (response.asInstanceOf[Response[String]], currentTime))
}
response
}
} else {
delegate.send(request)
}
}
def close(): Unit = {
cache.clear()
delegate.close()
}
def responseMonad: MonadError[Identity] = delegate.responseMonad
}
def cachingExample(): Unit = {
val cachingBackend = new CachingBackend(DefaultSyncBackend())
// Execute same request multiple times
val url = uri"https://httpbin.org/get"
println("First request:")
val response1 = basicRequest.get(url).send(cachingBackend)
println("Second request (from cache):")
val response2 = basicRequest.get(url).send(cachingBackend)
println("Third request (from cache):")
val response3 = basicRequest.get(url).send(cachingBackend)
cachingBackend.close()
}
}
Practical Usage Patterns
API Integration Service Construction
import sttp.client4.*
import zio.*
import io.circe.*
import io.circe.generic.semiauto.*
// Actual API service integration example
case class GitHubRepository(
id: Long,
name: String,
full_name: String,
description: Option[String],
stargazers_count: Int,
language: Option[String]
)
case class SearchResult(
total_count: Int,
items: List[GitHubRepository]
)
implicit val repoDecoder: Decoder[GitHubRepository] = deriveDecoder[GitHubRepository]
implicit val searchDecoder: Decoder[SearchResult] = deriveDecoder[SearchResult]
class GitHubApiService(backend: SttpBackend[Task, Any]) {
private val baseUrl = uri"https://api.github.com"
def searchRepositories(query: String, sort: String = "stars", per_page: Int = 30): Task[SearchResult] = {
basicRequest
.get(uri"$baseUrl/search/repositories?q=$query&sort=$sort&per_page=$per_page")
.header("Accept", "application/vnd.github.v3+json")
.header("User-Agent", "sttp-example/1.0")
.response(asJson[SearchResult])
.send(backend)
.flatMap { response =>
response.body match {
case Right(result) => ZIO.succeed(result)
case Left(error) => ZIO.fail(new RuntimeException(s"GitHub API error: $error"))
}
}
}
def getRepository(owner: String, repo: String): Task[GitHubRepository] = {
basicRequest
.get(uri"$baseUrl/repos/$owner/$repo")
.response(asJson[GitHubRepository])
.send(backend)
.flatMap { response =>
response.body match {
case Right(repository) => ZIO.succeed(repository)
case Left(error) => ZIO.fail(new RuntimeException(s"Repository not found: $error"))
}
}
}
}
// Usage example
object GitHubServiceExample extends ZIOAppDefault {
def run = {
val program = for {
backend <- ZIO.service[SttpBackend[Task, Any]]
githubService = new GitHubApiService(backend)
// Search Scala projects
scalaRepos <- githubService.searchRepositories("language:scala", "stars", 10)
_ <- ZIO.log(s"Found ${scalaRepos.total_count} Scala repositories")
topRepos = scalaRepos.items.take(5)
_ <- ZIO.foreach(topRepos) { repo =>
ZIO.log(s"${repo.name}: ${repo.stargazers_count} stars")
}
// Get specific repository details
sttpRepo <- githubService.getRepository("softwaremill", "sttp")
_ <- ZIO.log(s"sttp repository: ${sttpRepo.description.getOrElse("No description")}")
} yield ()
program.provide(
HttpClientZioBackend.layer()
)
}
}