Skip to main content
Version: 2.x

Overview

ZIO HTTP offers an expressive API for creating HTTP applications. It uses a domain-specific language (DSL) to define routes and handlers. Both server and client are designed in terms of HTTP as a function, so they are functions from Request to Response.

Core Concepts

ZIO HTTP has powerful functional domains that help in creating, modifying, and composing apps easily. Let's take a look at the core domain:

  • Routes - A collection of Routes. If the error type of the routes is Response, then they can be served.
  • Route - A single route that can be matched against an HTTP Request and produce a Response. It comprises a RoutePattern and a Handler:
    1. RoutePattern - A pattern that can be matched against an HTTP Request. It is a combination of Method and PathCodec which can be used to match the Method and Path of the Request.
    2. Handler - A function that can convert a Request into a Response.

Let's see each of these concepts inside a simple example:

import zio._
import zio.http._

object ExampleServer extends ZIOAppDefault {

// A route that matches GET requests to /greet
// It doesn't require any service from the ZIO environment
// so the first type parameter is Any
// All its errors are handled so the second type parameter is Nothing
val greetRoute: Route[Any, Nothing] =
// The whole Method.GET / "greet" is a RoutePattern
Method.GET / "greet" ->
// The handler is a function that takes a Request and returns a Response
handler { (req: Request) =>
val name = req.queryParamToOrElse("name", "World")
Response.text(s"Hello $name!")
}

// A route that matches POST requests to /echo
// It doesn't require any service from the ZIO environment
// It is an unhandled route so the second type parameter is something other than Nothing
val echoRoute: Route[Any, Throwable] =
Method.POST / "echo" -> handler { (req: Request) =>
req.body.asString.map(Response.text(_))
}

// The Routes that don't require any service from the ZIO environment,
// so the first type parameter is Any.
// All the errors are handled by turning them into a Response.
val routes: Routes[Any, Response] =
// List of all the routes
Routes(greetRoute, echoRoute)
// Handle all unhandled errors
.handleError(e => Response.internalServerError(e.getMessage))

// Serving the routes using the default server layer on port 8080
def run = Server.serve(routes).provide(Server.default)
}

1.Routes

The Routes is a collection of Route values. It can be created using its default constructor:

import zio.http._

val routes: Routes[Any, Response] =
Routes(greetRoute, echoRoute)
.handleError(e => Response.internalServerError(e.getMessage))

The Handler and Route can be transformed to Routes by the .toRoutes method. To serve the routes, all errors should be handled by converting them into a Response using for example the .handleError method.

For handling routes, ZIO HTTP has a Routes value, which allows us to aggregate a collection of individual routes. Behind the scenes, ZIO HTTP builds an efficient prefix-tree whenever needed to optimize dispatch.

2. Route

Each Route is a combination of a RoutePattern and a Handler. The RoutePattern is a combination of a Method and a PathCodec that can be used to match the method and path of the request. The Handler is a function that can convert a Request into a Response.

The PathCodec can be parameterized to extract values from the path. In such cases, the Handler should be a function that accepts the extracted values besides the Request:

import zio.http._

val routes = Routes(
Method.GET / "user" / int("id") ->
handler { (id: Int, req: Request) =>
Response.text(s"Requested User ID: $id")
}
)

To learn more about routes, see the Routes page.

3. Handler

The Handler describes the transformation from an incoming Request to an outgoing Response:

val helloHandler =
handler { (_: Request) =>
Response.text("Hello World!")
}

The Handler can be effectful, in which case it should be a function that returns a ZIO effect, e.g.:

val randomGeneratorHandler = 
handler { (_: Request) =>
Random.nextIntBounded(100).map(_.toString).map(Response.text(_))
}

There are several ways to create a Handler, to learn more about handlers, see the Handlers page.

Accessing the Request

To access the request, just create a handler that accepts the request:

import zio.http._
import zio._

val routes = Routes(
Method.GET / "fruits" / "a" -> handler { (req: Request) =>
Response.text("URL:" + req.url.path.toString + " Headers: " + req.headers)
},

Method.POST / "fruits" / "a" -> handler { (req: Request) =>
req.body.asString.map(Response.text(_))
}
)

To learn more about the request, see the Request page.

Accessing Services from The Environment

ZIO HTTP is built on top of ZIO, which means that we can access services from the environment in our handlers. For example, we can access a Ref[Int] service to create a simple counter:

import zio._
import zio.http._

object CounterExample extends ZIOAppDefault {
val routes: Routes[Ref[Int], Response] =
Routes(
Method.GET / "count" / int("n") ->
handler { (n: Int, _: Request) =>
for {
ref <- ZIO.service[Ref[Int]]
res <- ref.updateAndGet(_ + n)
} yield Response.text(s"Counter: $res")
},
)

def run = Server.serve(routes).provide(Server.default, ZLayer.fromZIO(Ref.make(0)))
}

Finally, we should provide the required services to the server using the provide method. In the above example, we provided the Ref[Int] service using the ZLayer.fromZIO method.

WebSocket Connection

To handle WebSocket connections, we can use Handler.webSocket to create a socket app. To create a socket app, we need to create a socket that accepts WebSocketChannel and produces ZIO. Finally, we need to convert socketApp to Response using toResponse, so that we can run it like any other HTTP app.

The below example shows a simple socket app, which sends WebsSocketTextFrame "BAR" on receiving WebsSocketTextFrame "FOO":

import zio.http._
import zio.stream._
import zio._

val socket =
Handler.webSocket { channel =>
channel.receiveAll {
case ChannelEvent.Read(WebSocketFrame.Text("FOO")) =>
channel.send(ChannelEvent.Read(WebSocketFrame.text("BAR")))
case _ =>
ZIO.unit
}
}

val routes =
Routes(
Method.GET / "greet" / string("name") -> handler { (name: String, req: Request) =>
Response.text(s"Greetings {$name}!")
},
Method.GET / "ws" -> handler(socket.toResponse)
)

We have a more detailed explanation of the WebSocket connection on the Socket page.

Server

As we have seen how to create HTTP apps, the only thing left is to run an HTTP server and serve requests.

ZIO HTTP provides a way to set configurations for our server. The server can be configured according to the leak detection level, request size, address etc.

To launch our app, we need to start the server on a port. The below example shows a simple HTTP app that responds with empty content and a 200 status code, deployed on port 8090 using Server.start:

import zio.http._
import zio._

object HelloWorld extends ZIOAppDefault {
val routes = Handler.ok.toRoutes

override def run =
Server.serve(routes).provide(Server.defaultWithPort(8090))
}

Finally, we provided the default server with the port 8090 to the app. To learn more about the server, see the Server page.

Client

Besides creating HTTP apps, ZIO HTTP also provides a way to create HTTP clients. The client can be used to send requests to the server and receive responses:

import zio._
import zio.http._

object ClientExample extends ZIOAppDefault {

val app =
for {
client <- ZIO.serviceWith[Client](_.host("localhost").port(8090))
response <- client.batched(Request.get("/"))
_ <- ZIO.debug("Response Status: " + response.status)
} yield ()

def run = app.provide(Client.default)
}

In the above example, we obtained the Client service from the environment and sent a GET request to the server. Finally, to run the client app, we provided the default Client and Scope services to the app. For more information about the client, see the Client page.