पाठ 22 / 25

Building HTTP Services

Choose a web stack and build a small JSON API.

Scala web stacks

Scala offers several web stacks for different styles. Play Framework is a full-stack MVC framework, popular for traditional web apps. http4s (with Cats Effect) and ZIO HTTP are functional, typed HTTP libraries used for high-throughput APIs. Tapir describes endpoints as values, then interprets them into servers (http4s, Pekko HTTP, Netty, ZIO HTTP), clients and OpenAPI documentation from the same definition. Pekko HTTP (or Akka HTTP) suits actor-based systems, and Cask from the Li Haoyi ecosystem is a lightweight, Flask-like option for simple services. For JSON, common libraries are circe, jsoniter-scala (very fast) and upickle; for databases, Doobie and Skunk (functional), Slick and ScalikeJDBC, or Quill and Magnum. Whatever the stack, keep the domain logic in pure functions and ADTs, keep HTTP and database code at the edges, validate inputs into domain types, and map domain errors to HTTP status codes in one place.

Layers of a Scala service

HTTP routes call pure domain logic, and repositories handle persistence at the edge.

A three-layer stack: a top layer with route icons, a middle core with a gear, and a bottom layer with a database cylinder.
Figure 8.1 — Routes, domain core and persistence.

A small JSON API with Cask and upickle

Routes, validation into domain types and status codes.

//> using dep com.lihaoyi::cask:0.10.2
//> using dep com.lihaoyi::upickle:4.1.0
import upickle.default.*

case class NewOrder(customerId: String, totalPaise: Long) derives ReadWriter
case class Order(id: String, customerId: String, totalPaise: Long) derives ReadWriter

object OrderApi extends cask.MainRoutes:
  private var orders = Map.empty[String, Order]   // in-memory store for the example

  @cask.get("/orders/:id")
  def getOrder(id: String) =
    orders.get(id) match
      case Some(o) => cask.Response(write(o), headers = Seq("Content-Type" -> "application/json"))
      case None    => cask.Response(s"order $id not found", statusCode = 404)

  @cask.post("/orders")
  def create(request: cask.Request) =
    val input = read[NewOrder](request.text())
    if input.totalPaise <= 0 then cask.Response("total must be positive", statusCode = 400)
    else
      val order = Order(s"o-${orders.size + 1}", input.customerId, input.totalPaise)
      orders = orders.updated(order.id, order)
      cask.Response(write(order), statusCode = 201, headers = Seq("Content-Type" -> "application/json"))

  initialize()

// run with: scala run .   then POST /orders on port 8080

One definition, many uses

With Tapir, the same endpoint value generates the server route, a typed client and OpenAPI docs, so documentation never drifts from the implementation.

त्वरित जाँच: What is a key benefit of Tapir?

  • It is a database driver
  • It replaces the JVM
  • Endpoints are defined once as values and interpreted into servers, clients and OpenAPI docs
  • It formats code
Answer

Endpoints are defined once as values and interpreted into servers, clients and OpenAPI docs — Tapir separates endpoint descriptions from their interpreters.