HTTP

The HTTP surface is the one you already know how to hold: Request, Response, Routes, Handler, Middleware, Server, Client. Bodies on the wire are streams. Body.asString / asBytes are for in-memory bodies; use collect otherwise. maxBodyBytes is a cap, not a buffer.

If you are building a hub, start at The hub and come back here for the knobs.

Routes and the path DSL

Method.GET / "users" / int("id") is a PathCodec. int, long, string, uuid, and trailing are the captures. Unmatched paths are 404; wrong method is 405.

{
  val routes = Routes(
    Method.GET / "users" / int("id") -> { (id: Int) =>
      ZIO.succeed(Response.text(id.toString))
    }
  )
  for
    ok   <- routes(Request.get("/users/3"))
    miss <- routes(Request.get("/users/ada"))
  yield (ok.body.asString, miss.status)
}
(3,Status(404,Not Found))
PathCodec
GET "shows" / int("id")
/shows/1
match  id = 1

handler runs

Middleware

@@ wraps this Routes value. ++ is try left, then right on 404/405. Gzip, decompress, CORS, auth, and Middleware.timeout attach to whoever you wrap. Global gzip is all @@ Middleware.compress(), which is still wrapping routes, not a server flag. Partial gzip is (api ++ files) @@ Middleware.compress() ++ sse.

Why that split exists is on What stays open.

{
  val routes =
    Routes(Method.GET / "x" -> Handler.text("ok")) @@ Middleware.requestId()
  routes(Request.get("/x")).map { res =>
    res.header("X-Request-Id").exists(_.nonEmpty)
  }
}
true
Middleware @@
routes @@ (Middleware.requestId() ++ Middleware.cors())

request → cors → requestId → handler
cors is outer. requestId still stamps X-Request-Id.

Server

Server.install / Server.serve are ordinary ZIO fibers on every platform. JVM accept defaults to Loom (JvmScheduler.Loom); JvmScheduler.Default still binds. JS is Node net / tls. Native is POSIX sockets plus OpenSSL. TLS is a compose-time layer, not a protocol flag: Server.serve(app).provide(Server.Config.defaults, Tls.pem(certPem, keyPem)). HTTP/2 (ALPN h2) is the JVM bind. JS and Native serve HTTP/1.1 on that same Routes value. Idle, header, and connection caps live on Server.Config. See What stays open.

{
  val routes = Routes(Method.GET / "health" -> Handler.text("ok"))
  ZIO.scoped {
    Server
      .install(routes, Server.Config.default.copy(host = "127.0.0.1", port = 0))
      .flatMap { server =>
        server.port.flatMap { port =>
          Client.get(s"http://127.0.0.1:$port/health").map { res =>
            (res.status, res.body.asString)
          }
        }
      }
  }
}
(Status(200,OK),ok)

Client, files, compression

Client.get / Client.batched are the HTTP/1.1 client. JVM pools sockets. JS uses fetch. Native opens a POSIX (or TLS) socket per call. One-shot helpers take Client.Config (default named values, including connect and read timeouts). A caller-given file is Files.fromPath (jailed under SafePath for directory roots). Compression is @@ Middleware.compress (gzip in core). Add heddle-brotli and pass Brotli.compressor when you want br. Incoming Content-Encoding is @@ Middleware.decompress(maxBytes = ...).

Brotli.encode(zio.Chunk.fromArray("hi".getBytes("UTF-8"))).nonEmpty
true