scalaintermediate

HTTP Server with http4s

Build HTTP servers with http4s: routes, middleware, JSON, and streaming responses.

scala
import cats.effect.*
import org.http4s.*
import org.http4s.dsl.io.*
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.circe.*
import org.http4s.circe.CirceEntityCodec.*
import io.circe.generic.auto.*
import io.circe.syntax.*
import com.comcast.ip4s.*

case class User(id: Long, name: String, email: String)
case class CreateUser(name: String, email: String)
case class ErrorResponse(error: String)

object Api extends IOApp.Simple:
  // In-memory store
  val store = Ref.unsafe[IO, (Long, Map[Long, User])]((1L, Map.empty))

  // Routes
  val userRoutes: HttpRoutes[IO] = HttpRoutes.of[IO] {
    // GET /users
    case GET -> Root / "users" =>
      for
        (_, users) <- store.get
        resp <- Ok(users.values.toList.asJson)
      yield resp

    // GET /users/:id
    case GET -> Root / "users" / LongVar(id) =>
      for
        (_, users) <- store.get
        resp <- users.get(id) match
          case Some(user) => Ok(user.asJson)
          case None => NotFound(ErrorResponse(s"User $id not found").asJson)
      yield resp

    // POST /users
    case req @ POST -> Root / "users" =>
      for
        body <- req.as[CreateUser]
        user <- store.modify { (nextId, users) =>
          val user = User(nextId, body.name, body.email)
          ((nextId + 1, users + (nextId -> user)), user)
        }
        resp <- Created(user.asJson)
      yield resp

    // DELETE /users/:id
    case DELETE -> Root / "users" / LongVar(id) =>
      for
        existed <- store.modify { (nextId, users) =>
          ((nextId, users - id), users.contains(id))
        }
        resp <- if existed then NoContent() else NotFound()
      yield resp
  }

  // Health check
  val healthRoutes: HttpRoutes[IO] = HttpRoutes.of[IO] {
    case GET -> Root / "health" =>
      Ok(Map("status" -> "ok").asJson)
  }

  // Middleware: request logging
  def requestLogger(routes: HttpRoutes[IO]): HttpRoutes[IO] =
    HttpRoutes { req =>
      cats.data.OptionT.liftF(
        IO.println(s"${req.method} ${req.uri}")
      ) >> routes.run(req)
    }

  // Combine routes
  val app: HttpApp[IO] = (
    requestLogger(userRoutes) <+> healthRoutes
  ).orNotFound

  def run: IO[Unit] =
    EmberServerBuilder
      .default[IO]
      .withHost(host"0.0.0.0")
      .withPort(port"8080")
      .withHttpApp(app)
      .build
      .use(_ => IO.println("Server started on :8080") >> IO.never)
      .void

Sponsored

Railway

Use Cases

  • REST API development with http4s
  • Functional HTTP servers
  • Middleware and route composition

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.