http4s
Minimal and idiomatic HTTP service library for Scala. Pure functional approach based on cats-effect and FS2 streaming library. Emphasizes composability, achieving robust HTTP communication through I/O management. Scala.js and Scala Native compatible.
GitHub Overview
http4s/http4s
A minimal, idiomatic Scala interface for HTTP
Topics
Star History
Library
http4s
Overview
http4s is "a minimal, idiomatic Scala interface for HTTP" developed as a type-safe, functional streaming HTTP library. Positioned as Scala's equivalent to Ruby Rack, Python WSGI, Haskell WAI, and Java Servlets, it provides a comprehensive HTTP ecosystem. Built on Cats Effect (effect management) and FS2 (streaming), it implements functional HTTP programming through immutable request/response models, typeclass-based entity codecs, and rich DSL. Supporting both client and server functionality with compatibility for Scala.js and Scala Native, it offers a complete functional programming approach to HTTP communication.
Details
http4s 2025 edition has established itself as the definitive HTTP communication solution based on functional programming paradigms. The combination of Cats Effect and FS2 enables type-safe, effect-managed, and memory-efficient streaming processing. The Ember client supports HTTP/1.x and HTTP/2 with automated connection pooling and resource management. The immutable Request/Response model ensures safety in concurrent processing, while typeclass-based entity encoders/decoders support automatic conversion of JSON, XML, and other formats. The modular design allows selection of different backends (Ember, Blaze, Netty, etc.), and a rich middleware ecosystem addresses enterprise-level requirements.
Key Features
- Pure Functional Approach: Effect management via Cats Effect and streaming via FS2
- Type-safe HTTP Operations: Safe HTTP communication through compile-time type checking
- Immutable Request/Response: Safety and predictability in concurrent processing
- Streaming Support: Efficient processing of large data in constant space
- Modular Design: Interchangeable client/server backends
- Cross-platform: Support for Scala.js and Scala Native
Pros and Cons
Pros
- Complete integration with functional programming in the Scala ecosystem
- Type safety enabling compile-time error detection and runtime safety
- Efficient large data processing capabilities through FS2 streaming
- Connection leak prevention through Cats Effect resource management
- Efficient implementation of cross-cutting concerns through rich middleware
- Full-stack development possibilities with ScalaJS/Native support
Cons
- High learning cost for functional programming concepts (monads, functors, etc.)
- Need to acquire ecosystem-specific concepts like Cats Effect and FS2
- Limited community size compared to HTTP libraries in other languages
- Potentially longer JVM warm-up time compared to traditional libraries
- Additional design required for integration with non-functional Scala codebases
- Effect stack understanding can become difficult during debugging
Reference Pages
Code Examples
Project Setup and Installation
// build.sbt
scalaVersion := "2.13.12" // or 3.x, 2.12.x
val http4sVersion = "0.23.26"
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-ember-client" % http4sVersion,
"org.http4s" %% "http4s-ember-server" % http4sVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion,
"org.http4s" %% "http4s-circe" % http4sVersion,
"io.circe" %% "circe-generic" % "0.14.6"
)
// Uncomment for Scala 2.12.x
// scalacOptions ++= Seq("-Ypartial-unification")
// Only necessary for SNAPSHOT releases
// resolvers += Resolver.sonatypeOssRepos("snapshots")
Basic Imports and Client Creation
import cats.effect._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.ember.client.EmberClientBuilder
import org.http4s.client.Client
// Basic client creation pattern
object BasicHttpClient extends IOApp.Simple {
def printHello(client: Client[IO]): IO[Unit] =
client
.expect[String]("http://localhost:8080/hello/http4s")
.flatMap(IO.println)
val run: IO[Unit] = EmberClientBuilder
.default[IO]
.build
.use(client => printHello(client))
}
// Simple client for REPL (not recommended for production)
import org.http4s.client.JavaNetClientBuilder
// For REPL or mdoc use only!
val httpClient: Client[IO] = JavaNetClientBuilder[IO].create
Basic HTTP Requests (GET/POST/PUT/DELETE)
import cats.effect.IO
import org.http4s._
import org.http4s.circe._
import io.circe.generic.auto._
import io.circe.syntax._
// Basic GET request
val basicGet: IO[String] =
httpClient.expect[String](uri"https://api.example.com/users")
// GET request with query parameters
val withParams: IO[String] = {
val uri = uri"https://api.example.com/users"
.withQueryParam("page", 1)
.withQueryParam("limit", 10)
.withQueryParam("sort", "created_at")
httpClient.expect[String](uri)
}
// GET request with custom headers
import org.http4s.headers._
val customHeaderRequest: IO[String] = {
val request = Request[IO](
method = Method.GET,
uri = uri"https://api.example.com/protected",
headers = Headers(
Authorization(Credentials.Token(AuthScheme.Bearer, "your-token")),
Accept(MediaType.application.json),
Header.Raw(ci"X-API-Version", "v2")
)
)
httpClient.expect[String](request)
}
// POST request (sending JSON)
case class User(name: String, email: String, age: Int)
case class CreateUserResponse(id: Long, name: String, email: String)
implicit val createUserResponseDecoder: EntityDecoder[IO, CreateUserResponse] = jsonOf
val createUser: IO[CreateUserResponse] = {
val newUser = User("John Doe", "[email protected]", 30)
val postRequest = Request[IO](
method = Method.POST,
uri = uri"https://api.example.com/users"
).withEntity(newUser.asJson)
.withHeaders(Authorization(Credentials.Token(AuthScheme.Bearer, "your-token")))
httpClient.expect[CreateUserResponse](postRequest)
}
// Form data POST
val loginRequest: IO[String] = {
val formData = UrlForm(
"username" -> "testuser",
"password" -> "secret123"
)
val request = Request[IO](
method = Method.POST,
uri = uri"https://api.example.com/login"
).withEntity(formData)
httpClient.expect[String](request)
}
// PUT request (data update)
val updateUser: IO[User] = {
val updatedData = User("Jane Doe", "[email protected]", 31)
val putRequest = Request[IO](
method = Method.PUT,
uri = uri"https://api.example.com/users/123"
).withEntity(updatedData.asJson)
.withHeaders(Authorization(Credentials.Token(AuthScheme.Bearer, "your-token")))
httpClient.expect[User](putRequest)
}
// DELETE request
val deleteUser: IO[Unit] = {
val deleteRequest = Request[IO](
method = Method.DELETE,
uri = uri"https://api.example.com/users/123"
).withHeaders(Authorization(Credentials.Token(AuthScheme.Bearer, "your-token")))
httpClient.successful(deleteRequest).flatMap { success =>
if (success) IO.println("User deleted successfully")
else IO.raiseError(new RuntimeException("Delete operation failed"))
}
}
Error Handling and Resource Management
import cats.effect._
import cats.syntax.all._
import org.http4s.client.UnexpectedStatus
// Comprehensive error handling
def safeRequest[A](request: Request[IO])(implicit decoder: EntityDecoder[IO, A]): IO[Either[String, A]] = {
httpClient.expect[A](request).attempt.map {
case Right(response) => Right(response)
case Left(_: UnexpectedStatus) => Left("HTTP status error")
case Left(_: org.http4s.client.ConnectionFailure) => Left("Connection error")
case Left(_: java.util.concurrent.TimeoutException) => Left("Timeout")
case Left(ex) => Left(s"Unexpected error: ${ex.getMessage}")
}
}
// Usage example
val safeGetUser: IO[Either[String, User]] = {
val request = Request[IO](Method.GET, uri"https://api.example.com/users/123")
safeRequest[User](request)
}
// Request with retry functionality
import scala.concurrent.duration._
def requestWithRetry[A](
request: Request[IO],
maxRetries: Int = 3,
backoff: FiniteDuration = 1.second
)(implicit decoder: EntityDecoder[IO, A]): IO[A] = {
def attempt(retriesLeft: Int): IO[A] = {
httpClient.expect[A](request).handleErrorWith { error =>
if (retriesLeft > 0) {
IO.println(s"Request failed, retrying in ${backoff.toSeconds} seconds... ($retriesLeft attempts left)") >>
IO.sleep(backoff) >>
attempt(retriesLeft - 1)
} else {
IO.raiseError(error)
}
}
}
attempt(maxRetries)
}
// Custom timeout configuration
val timeoutClient: Resource[IO, Client[IO]] =
EmberClientBuilder
.default[IO]
.withTimeout(30.seconds)
.withIdleTimeInPool(5.minutes)
.build
// Connection pool configuration
val pooledClient: Resource[IO, Client[IO]] =
EmberClientBuilder
.default[IO]
.withMaxTotal(100)
.withMaxPerDestination(10)
.withMaxIdleTime(1.minute)
.build
Client Middleware and Advanced Features
import org.http4s.client.middleware._
import org.typelevel.ci._
// Logging middleware
val loggedClient: Resource[IO, Client[IO]] =
EmberClientBuilder
.default[IO]
.build
.map(Logger.apply[IO](logHeaders = true, logBody = true))
// Redirect following middleware
val redirectClient: Resource[IO, Client[IO]] =
EmberClientBuilder
.default[IO]
.build
.map(FollowRedirect(maxRedirects = 3))
// Retry middleware
import org.http4s.client.middleware.Retry._
val retryClient: Resource[IO, Client[IO]] =
EmberClientBuilder
.default[IO]
.build
.map(Retry(RetryPolicy.exponentialBackoff(maxWait = 10.seconds, maxRetry = 3)))
// Creating custom middleware
def addUserAgentHeader(underlying: Client[IO]): Client[IO] =
Client[IO] { req =>
underlying.run(
req.withHeaders(Header.Raw(ci"User-Agent", "MyApp/1.0 (http4s Scala)"))
)
}
// Auto-refresh authentication token middleware
class AuthRefreshMiddleware(
clientId: String,
clientSecret: String,
tokenEndpoint: Uri
) {
private val tokenRef: Ref[IO, Option[String]] = Ref.unsafe(None)
def apply(underlying: Client[IO]): Client[IO] = Client[IO] { req =>
for {
token <- ensureValidToken
authedRequest = req.withHeaders(
Authorization(Credentials.Token(AuthScheme.Bearer, token))
)
response <- underlying.run(authedRequest)
} yield response
}
private def ensureValidToken: IO[String] = {
tokenRef.get.flatMap {
case Some(token) => IO.pure(token)
case None => refreshToken
}
}
private def refreshToken: IO[String] = {
val authRequest = Request[IO](
method = Method.POST,
uri = tokenEndpoint
).withEntity(UrlForm(
"grant_type" -> "client_credentials",
"client_id" -> clientId,
"client_secret" -> clientSecret
))
// Token acquisition and caching logic
IO.pure("new-token") // Simplified implementation
}
}
// Combined middleware
val enhancedClient: Resource[IO, Client[IO]] =
EmberClientBuilder
.default[IO]
.build
.map(addUserAgentHeader)
.map(FollowRedirect(maxRedirects = 5))
.map(Logger.apply[IO](logHeaders = false, logBody = false))
Concurrent Processing and Streaming
import cats.syntax.parallel._
import fs2.Stream
// Parallel request processing
def fetchMultipleUsers(userIds: List[Long]): IO[List[User]] = {
userIds.parTraverse { id =>
httpClient.expect[User](uri"https://api.example.com/users" / id.toString)
}
}
// Streaming response processing
def streamLargeDataset(client: Client[IO]): Stream[IO, String] = {
val request = Request[IO](
method = Method.GET,
uri = uri"https://api.example.com/large-dataset"
)
client.stream(request).flatMap(_.body.through(fs2.text.utf8.decode))
}
// Pagination-aware data fetching
case class PagedResponse[T](items: List[T], hasMore: Boolean, nextPage: Option[Int])
def fetchAllPages[T: EntityDecoder[IO, *]](
baseUri: Uri,
initialPage: Int = 1
): IO[List[T]] = {
def fetchPage(page: Int): IO[PagedResponse[T]] = {
val uri = baseUri.withQueryParam("page", page).withQueryParam("per_page", 100)
httpClient.expect[PagedResponse[T]](uri)
}
def loop(page: Int, acc: List[T]): IO[List[T]] = {
fetchPage(page).flatMap { response =>
val newAcc = acc ++ response.items
if (response.hasMore) {
loop(page + 1, newAcc)
} else {
IO.pure(newAcc)
}
}
}
loop(initialPage, List.empty)
}
// Server-Sent Events (SSE) processing
import org.http4s.dom.WebSocketEvent
def subscribeToEvents(client: Client[IO]): Stream[IO, String] = {
val request = Request[IO](
method = Method.GET,
uri = uri"https://api.example.com/events",
headers = Headers(Accept(MediaType.text.`event-stream`))
)
client.stream(request)
.flatMap(_.body.through(fs2.text.utf8.decode))
.through(fs2.text.lines)
.filter(_.startsWith("data: "))
.map(_.drop(6)) // Remove "data: " prefix
}
Practical Application Example
import cats.effect.{ExitCode, IOApp}
// Comprehensive API client application
object ApiClientApp extends IOApp {
case class Config(
baseUrl: String,
apiKey: String,
timeout: FiniteDuration,
maxRetries: Int
)
class ApiClient(client: Client[IO], config: Config) {
private def authenticatedRequest(request: Request[IO]): Request[IO] =
request.withHeaders(
Authorization(Credentials.Token(AuthScheme.Bearer, config.apiKey)),
Header.Raw(ci"Accept", "application/json"),
Header.Raw(ci"User-Agent", "http4s-client/1.0")
)
def getUsers(page: Int = 1, limit: Int = 50): IO[List[User]] = {
val uri = Uri.unsafeFromString(config.baseUrl) / "users"
.withQueryParam("page", page)
.withQueryParam("limit", limit)
val request = authenticatedRequest(Request[IO](Method.GET, uri))
client.expect[List[User]](request)
}
def createUser(user: User): IO[User] = {
val uri = Uri.unsafeFromString(config.baseUrl) / "users"
val request = authenticatedRequest(
Request[IO](Method.POST, uri).withEntity(user.asJson)
)
client.expect[User](request)
}
def uploadFile(filePath: String, contentType: MediaType): IO[String] = {
import java.nio.file.Paths
val file = Paths.get(filePath)
val multipart = Multipart[IO](
Vector(
Part.fileData[IO]("file", file, `Content-Type`(contentType))
)
)
val uri = Uri.unsafeFromString(config.baseUrl) / "upload"
val request = authenticatedRequest(
Request[IO](Method.POST, uri).withEntity(multipart)
)
client.expect[String](request)
}
}
def run(args: List[String]): IO[ExitCode] = {
val config = Config(
baseUrl = "https://api.example.com",
apiKey = sys.env.getOrElse("API_KEY", "default-key"),
timeout = 30.seconds,
maxRetries = 3
)
EmberClientBuilder
.default[IO]
.withTimeout(config.timeout)
.build
.use { baseClient =>
val enhancedClient = Logger.apply[IO](logHeaders = true, logBody = false)(
FollowRedirect(maxRedirects = 3)(baseClient)
)
val apiClient = new ApiClient(enhancedClient, config)
for {
users <- apiClient.getUsers(page = 1, limit = 10)
_ <- IO.println(s"Fetched users count: ${users.length}")
newUser = User("New User", "[email protected]", 25)
created <- apiClient.createUser(newUser)
_ <- IO.println(s"Created user: $created")
} yield ExitCode.Success
}
}
}