Recently I experimented with Scala 3, ZIO 2, zhttp, zio-json and Laminar. You can see the proof of concept here: https://github.com/adrianfilip/reservation-booker.
I made the following setup:
- backend app with Scala 3 + ZIO 2 + zhttp + zio-json that is called by a
- frontend app Scala 3 + ZIO 2 + Laminar + zio-json.
- Part of the domain was split in it’s own shared (jvm,js) module using Scala 3 + zio-json to avoid code duplication.
I think you probably are more interested in the code but I want to leave you with a few of the things I found interesting and a few highlights:
1. More precision when defining APIs with ZIO in Scala 3
I no longer had to default to supertypes at lower levels and loose precision. This means more info about how something can fail at all levels.
Notice that in the RoomRepository update operation I can use a subset of the RoomDomainError.
2. Easy to separate frontend domains by widening or narrowing stream event types
In the frontend we can consider the pages/subpages as the domains and define boundaries around them. How would that be done?
My approach was to juggle with several busses:
- application event bus – EventBus[Event]
- page (domain) event bus – EventBus[XDomainEvent | SecurityEvent]
– All calls can fail for security reasons and I want that handled at a top level instead of the page level so SecurityEvent’s are applicable in page event busses
- component (local) event bus – EventBus[LocalEvent | BackendResponse | SecurityEvent | ServiceError]
- I introduced a bus at this level also because I don’t like to have logic split throughout the code of the page. This way I can define a handler for the bus at the component level and have everything in one place.
Let’s say we have the following structure for events:
The application event bus would support Event above meaning all types of events.
The page event bus for MyReservations would support only MyResevationsPageEvents because in the MyReservations page we don’t really care about other domains.
We can actually go even further and narrow what events are supported at a component level if we think the page is too broad for our context.
We can do that by restricting the types of events that go in and come out of the MakeReservation component that is in the MyReservation page. As you can see in the picture below this restriction is basically a filtering of what can be consumed from and published to the app event bus.
I did not see a way to turn an EventBus[A] into an EventBus[B] directly so I made an EventStream and an Observer to cover consuming and producing (using map & contramap does the trick here). In practice it amounts to the same thing as the EventStream + Observer used together form basically an event bus.
At the component level we can add a component (or local) event bus for dealing with events returned by our backend calls and also any other local events we may define.
Here is how it can look like for MakeReservation component.
That is a lot of event types (we could make it look nicer if we wanted to. I see it as an advantage that we can defer the decision of how to structure the types until the solution is clearer). But what is happening there?
We defined an event bus that we can use for:
- backend responses: GetAllBuildingsResponse, GetFilteredRoomsResponse, AddReservationResponse, ServiceErrors, SecurityErrors
- component (local) events: SelectedBuilding, SelectedRoom, MakeReservation.RefreshAddReservationForm, SelectedFloor
That’s a lot of text about event busses but nothing about handlers so let’s catch up but in reverse order of the presented busses this time.
Handlers correspond to each bus type.
- component (local) event handler
- defined at the component level (0-1/component)
- as you may have noticed the component bus has a lot of events that are only present locally most notably the backend responses. This means they are dealt with locally
- page (domain) event bus handler
- defined at the page level (1/page)
- app event bus handler
- defined at the application level (1/app)
3. Frontend – Backend interactions with Fetch + ZIO
I am using io.laminext.fetch as http client.
Http calls to the backend result in json responses when successful and for certain errors and in some situations (malformed response, network connectivity, …) in failures we have to work a bit extra to mould the response into something we can use.
You may have noticed above the ServiceErrors and SecurityErrors event types besides more intuitive ones like GetAllBuildingsResponse. In fact the entire frontend only works with event types defined in the Event sum type.
What I did was to convert every response either into the type I am expecting or into a ServiceError or a SecurityError. This is done by using the custom made makeRequest[B, A, E] operations like below.
My implementation can be found in the unfortunately named HttpClient – https://github.com/adrianfilip/reservation-booker/blob/master/booker-ui/src/main/scala/com/adrianfilip/booker/ui/services/HttpClient.scala.
4. ZIO STM is great for prototyping / stubbing also
It’s just easy to work with so I almost always end up using it for stubs/mocks.
5. zio-json and convenient shared domains
Being able to have shared domains is convenient but having to parse them in different ways is not. zio-json is a great fit for both backend and frontend use cases.
Here is an example of a case class with a Scala 3 enum for which codecs are made with zio-json.
This type is in a shared module (jvm, js) and can be used as is in both frontend and backend. Pretty straightforward.
6. zhttp composition
The composition is powerful on its own on top of that we do something like compose apps that we allow to fail in ways we then handle only once at the higher level.
7. Can create extension methods without boilerplate
8. zio.Runtime.unsafeRun was replaced
In the frontend app effects have to be evaluated to interact with the backend. In ZIO 2 this is done using zio.Unsafe like below.
9. Custom datepicker in Laminar
I could not find a Laminar datepicker so I made one. You can find it in the project here: https://github.com/adrianfilip/reservation-booker/blob/master/booker-ui/src/main/scala/com/adrianfilip/components/datepicker/Datepicker.scala
- If you want to know more about ZIO 2 I recommend: https://zio.dev.
- There is also a discord for ZIO you can check out https://discord.gg/2ccFBr4.
- If you want animations and other cool visual stuff in Laminar I recommend https://github.com/kitlangton/animus
- If you want to know more about Laminar I recommend https://laminar.dev.
- ZIO 2 running effects: https://zio.dev/overview/overview_running_effects
Linking the proof of concept again so you don’t have to scroll back up: https://github.com/adrianfilip/reservation-booker
You can find me on:
- Twitter: https://twitter.com/realAdrianFilip
- LinkedIn: https://www.linkedin.com/in/adrianfilip/
- GitHub: https://github.com/adrianfilip