Generating clients

As we’ve seen in Generating a Server, guardrail-generated servers establish a mapping between our business logic and a cordoned off subset of HTTP. This permits us to focus on our business logic, without getting overloaded with the complexities of managing such a large protocol. The same is true with guardrail generated HTTP Clients: from a consumer’s standpoint, HTTP calls should look like regular function calls, accepting domain-specific arguments and producing domain-specific results.

By generating minimal clients that only have enough business knowledge to map domain types to and from HTTP, opportunities for logical errors are effectively removed. While this does not eliminate logical errors entirely, establishing a firm boundary between the underlying protocol and hand-written code drastically reduces the scope of possible bugs.

The following is an example from the http4s client generator:

// Two constructors are provided, one accepting the `httpClient` and `Async`
// implicitly, the other accepting an explicit `httpClient`, but still
// accepting the `Async` implicitly
object UserClient {
  def apply[F[_]](host: String)(implicit F: Async[F], httpClient: Http4sClient[F]): UserClient[F] = new UserClient[F](host = host)(F = F, httpClient = httpClient)
  def httpClient[F[_]](httpClient: Http4sClient[F], host: String)(implicit F: Async[F]): UserClient[F] = new UserClient[F](host = host)(F = F, httpClient = httpClient)

class UserClient[F[_]](host: String)(implicit F: Async[F], httpClient: Http4sClient[F]) {
  val basePath: String = "/v2"
  def createUser(body: definitions.User, headers: List[Header] = List.empty): F[CreateUserResponse] = {
    val allHeaders = headers ++ List[Option[Header]]().flatten
    val req = Request[F](method = Method.POST, uri = Uri.unsafeFromString(host + basePath + "/user"), headers = Headers(allHeaders)).withEntity(body)(createUserEncoder){
      case =>
        F.pure(CreateUserResponse.Ok): F[CreateUserResponse]
      case resp =>
  def createUsersWithArrayInput(body: Vector[definitions.User], headers: List[Header] = List.empty): F[CreateUsersWithArrayInputResponse] = ???
  def createUsersWithListInput(body: Vector[definitions.User], headers: List[Header] = List.empty): F[CreateUsersWithListInputResponse] = ???
  def loginUser(username: String, password: String, headers: List[Header] = List.empty): F[LoginUserResponse] = ???
  def logoutUser(headers: List[Header] = List.empty): F[LogoutUserResponse] = ???
  def getUserByName(username: String, headers: List[Header] = List.empty): F[GetUserByNameResponse] = ???
  def updateUser(username: String, body: definitions.User, headers: List[Header] = List.empty): F[UpdateUserResponse] = ???
  def deleteUser(username: String, headers: List[Header] = List.empty): F[DeleteUserResponse] = ???

(See it in action: guardrail-dev/guardrail-sample-http4s, guardrail-dev/guardrail-sample-sbt-http4s-zio)

Separation of protocol-concerns from API-level concerns

As guardrail clients are built on top of any Http4s client type, client configuration is done the same way as you are already familiar with when using Http4s.

Check out the docs for Http4s Clients.

Prev: Generating a Server