Discussing with a brazilian friend about the situation in our country, we realised how difficult it is to find information about public spending, and when available, how difficult it can be to reason about it. Joining our forces, we decided to explore some data exposed by the Brazilian government, aiming to provide an easier way to visualise and understand how the public resources has been used. The starting point would be: finding some data to analyse, that is relatively easy (at least from a developer’s perspective) to collect. A good candidate is the (in literal translation, Transparency Portal), an initiative to make public data available via APIs or downloading CSV files. Portal da Transparência Is there a better way to learn about an API than writing a client for it? So let’s do it with + ! ZIO http4s client Why ZIO? After , someone asked me what has called my attention in the Scala ecosystem recently. I believe ZIO can be a game changer, because it is not “just for functional programmers”. Even though it is strongly based in functional principles, it doesn’t assume the users already understand functional concepts (this is just a !), which can be scary for new joiners. my talk in Scala UA Monad Among all the powerful features ZIO provides, it’s designed to be easy to use and adopt, what is from my perspective, by far, its best feature. ZIO Team! #ScalaThankYou Time to code, let’s start . defining a ZIO module The module HttpClient The API supports only GET requests, what makes the trait definition very simple: pdt.http io.circe. org.http4s.client. zio._ { = [ ] { rootUrl = [ ](uri: , parameters: [ , ]) ( d: [ ]): [ ] } : [ [ [ ]], , ] = ??? } package import Decoder import Client import object HttpClient type HttpClient Has Service trait Service protected val "http://www.transparencia.gov.br/api-de-dados/" def get T String Map String String implicit Decoder T Task T def http4s ZLayer Has Client Task Nothing HttpClient has only one method with arguments and , which will become part of the url in the format . It takes an implicit as well, used to decode the json result into . Service get[T] resource: String parameters: Map[String, String] "resource?key=value" io.circe.Decoder[T] T returns a , , which represents an effect that has no requirements, and may fail with a value, or succeed with a . get[T] zio.Task[T] a type alias for ZIO[Any, Throwable, T] Throwable T , we have: Following the module recipe = [ ] type HttpClient Has Service In simple terms, allows us to use our as a dependency. The next line makes it easier to understand: Has Service : [ [ [ ]], , ] = ??? def http4s ZLayer Has Client Task Nothing HttpClient The method will create a , which is very similar to ; it requires a to be built, won’t produce any errors (that’s what that means) and will return an implementation of our : , the one we defined using . http4s ZLayer data type ZIO Has[Client[Task]] Nothing Service HttpClient Has We should use type aliases to make more expressive as well. Knowing our layer can’t fail, we can use : ZLayer URLayer : [ [ [ ]], ] = ??? def http4s URLayer Has Client Task HttpClient In order to answer this question, we need to implement first. What will http4s actually return? HttpClient.Service The implementation Http4s Implementing the get request is straightforward: pdt.http io.circe. org.http4s. org.http4s.circe. .circeEntityDecoder org.http4s.client. org.http4s.client.dsl. zio._ zio.interop.catz._ [http] . [ ] { [ ](resource: , parameters: [ , ]) ( d: [ ]): [ ] = { uri = (path = rootUrl + resource).withQueryParams(parameters) client.expect[ ](uri.toString()) } package import Decoder import Uri import CirceEntityCodec import Client import Http4sClientDsl import import private final case ( ) class Http4s client: [ ] Client Task extends HttpClient Service with Http4sClientDsl Task def get T String Map String String implicit Decoder T Task T val Uri T Maybe you are scratching your head due to that . is built on top of the Cats Effect stack, therefore we need . import zio.interop.catz._ http4s the module for interoperability interop-catz An instance of this class can’t be created outside the package; the instance will be provided through our . Let’s go back to , it’s time to implement it! http ZLayer HttpClient.http4s Providing an through HttpClient.Service ZLayer Having a service definition, seems appropriate: ZLayer.fromService { : [ [ [ ]], ] = .fromService[ [ ], ] { http4sClient => (http4sClient) } } object HttpClient def http4s URLayer Has Client Task HttpClient ZLayer Client Task Service Http4s Okay, this layer makes our HttpClient available. How can we access it? Let’s start defining something that uses the client, a concrete example always makes learning easier :) A couple of useful helpers The first resource, is a good candidate: Acordos de Leniência GET returns an object; /acordos-leniencia/{id} GET (with filters as query params) returns a list of objects; /acordos-leniencia The rest of the API exposes basically the same for other resources, just with more filters. Knowing that, we can define two helpers, one for each case: { [ ](resource: , id: ) ( d: [ ]): [ , ] = .accessM[ ](_.get.get[ ]( , ())) [ ](resource: , parameters: [ , ] = ()) ( d: [ ]): [ , [ ]] = .accessM[ ](_.get.get[ [ ]](resource, parameters)) } object HttpClient // ... def get T String Long implicit Decoder T RIO HttpClient T RIO HttpClient T s" / " $resource $id Map def get T String Map String String Map implicit Decoder T RIO HttpClient List T RIO HttpClient List T effectfully accesses the environment of our effect, giving us , so we call the first get to access the effect wrapped by - our Service - while the second is the actual get request. RIO.accessM[HttpClient] Has[HttpClient.Service] Has get To make it clear, if we had a method, the code would be: post .accessM[ ](_.get.post[ ](resource, parameters)) RIO HttpClient T Alright, let’s make the whole thing work! A concrete HttpClient… client (?!?!?) Then again, is our resource. This is a case class for its possible filters (brazilian api, names in portuguese): Acordos de Leniência case ( ) class AcordoLenienciaRequest cnpjSancionado: [ ] = , nomeSancionado: [ ] = , situacao: [ ] = , dataInicialSancao: [ ] = , dataFinalSancao: [ ] = , pagina: = 1 Option String None Option String None Option String None Option LocalDate None Option LocalDate None Int And the response: case ( ) class AcordoLeniencia id: , nomeEmpresa: , dataInicioAcordo: , dataFimAcordo: , orgaoResponsavel: , cnpj: , razaoSocial: , nomeFantasia: , ufEmpresa: , situacaoAcordo: , quantidade: Long String LocalDate LocalDate String String String String String String Int couldn’t be simpler: AcordosLenienciaClient io.circe.generic.auto._ pdt.client.decoders.localDateDecoder pdt.http. .{ , get} pdt.domain.{ , => } pdt.http.implicits. zio._ { (id: ): [ , ] = get[ ]( , id) (request: ): [ , [ ]] = get[ ]( , request.parameters) } import import import HttpClient HttpClient import AcordoLeniencia AcordoLenienciaRequest ALRequest import HttpRequestOps import object AcordosLenienciaClient def by Long RIO HttpClient AcordoLeniencia AcordoLeniencia "acordos-leniencia" def by ALRequest RIO HttpClient List AcordoLeniencia AcordoLeniencia "acordos-leniencia" The implicit method transforms any request into a . . HttpRequestOps.parameters Map[String, String] Check it out how I used shapeless to do so Now we just need to put all the pieces together, sort out dependencies, this kind of thing. That happens at the end of the world… also known as . Main ZIO Modules, assemble! Here is a program that makes a request to get a list of : AcordoLeniencia program = { result <- .by( ()) _ <- putStrLn(result.toString()) } () val for AcordosLeniencia AcordoLenienciaRequest yield It requires a that produces an , which has as its own dependency. Let’s create the as a first: ZLayer HttpClient Client[Task] Client[Task] managed resource : [ [ [ ]]] = .runtime[ ].map { rts => .apply[ ]( .global) .resource .toManaged } private def makeHttpClient UIO TaskManaged Client Task ZIO Any implicit BlazeClientBuilder Task Implicits Now we can sort out the layers: httpClientLayer = makeHttpClient.toLayer.orDie http4sClientLayer = httpClientLayer >>> .http4s val val HttpClient and finally, provide our with the required layer: program program.provideSomeLayer[ ](http4sClientLayer) ZEnv Ready to go: program.foldM( e => putStrLn( ) *> .succeed( ), _ => .succeed( ) ) s"Execution failed with: " ${e.printStackTrace()} ZIO 1 ZIO 0 And this is how I built it. Some code is different here from the original, for learning purposes. . You can find the code on Github Before we jump to the conclusion, let’s consolidate what we’ve learnt adding a new dependency, a that prints in the console the url requested, and the error message if it fails. logger Adding a new dependency, step by step The module definition: Logger { = [ ] { (message: => ): [ ] (t: )(message: => ): [ ] } } object Logger type Logger Has Service trait Service def info String UIO Unit def error Throwable String UIO Unit The implementation, printing to the console: zio.console.{ => } . { (message: => ): [ ] = console.putStrLn(message) (t: )(message: => ): [ ] = { _ <- console.putStrLn(message) _ <- console.putStrLn(t.stackTrace) } () } import Console ConsoleZIO case ( ) class Console console: . ConsoleZIO Service extends Logger Service def info String UIO Unit def error Throwable String UIO Unit for yield makes the implementation available via : Logger ZLayer { : [ , ] = .fromService[ . , ] { console => (console) } } object Logger def console URLayer ConsoleZIO Logger ZLayer ConsoleZIO Service Service Console can now receive and use a instance: Http4s logger [http] . [ ] { [ ](resource: , parameters: [ , ]) ( d: [ ]): [ ] = { uri = (path = rootUrl + resource).withQueryParams(parameters) logger.info( ) *> client .expect[ ](uri.toString()) .foldM( e => logger.error(e)( ) *> .fail(e), .succeed(_)) } } private final case ( ) class Http4s logger: . , client: [ ] Logger Service Client Task extends HttpClient Service with Http4sClientDsl Task def get T String Map String String implicit Decoder T Task T val Uri s"GET REQUEST: " $uri T "Request failed" IO ZIO The layer needs to adapt: http4s { : [ [ [ ]], ] = .fromServices[ . , [ ], ] { (logger, http4sClient) => (logger, http4sClient) } } object HttpClient def http4s URLayer Logger with Has Client Task HttpClient ZLayer Logger Service Client Task Service Http4s Let’s feed our with the new dependency. The change is in the layer provided: program http4sClientLayer = (loggerLayer ++ httpClientLayer) >>> .http4s val HttpClient Done! Summary My first experience with ZIO has been very pleasant. In order to solve dependencies, the compiler plays on our side; every time something is missing, we have an error in compile time, with a clear indication of what is missing. Besides, makes dependency resolution extremely simple and extensible (think about adding a for example) without magic. ZLayer FileLogger Any suggestions to improve that code? Please share! References ZIO - Using modules and Layers Solving the Dependency Injection Problem with ZIO From idea to product with ZLayer ZIO, Http4s, Auth, Codecs and zio-test Http4s Client Originally published at https://juliano-alves.com on April 20, 2020.