In the previous two articles we have explored the basics of fp-ts and io-ts.
In this article we will build a practical example of an application using these
libraries. We will build a simple application for updating product:
Loading...
The application is very simple. It fetches a list of products from a external
source and displays them to the user. The user can then add or remove products
from the cart. The cart is limited to 10 items. The user can also remove items
from the cart.
Infrastructure Layer
The infrastructure layer is responsible for fetching data from external sources
or services. Normally this would be a database or a web service. In this example
i'm just returning a stringified JSON object, saves me having to mock a http
request.
One of the really nice things about fp-ts is that it allows you to compose the
fetching of data and address any errors that may occur. In this example Im using
TaskEither to represent the asynchronous nature of the request and the
possibility of an error. We are using chainEitherK to convert the Either
returned by decode into a TaskEither. This allows us to compose the
TaskEither with other TaskEither's using chain and map.
The pipe makes it very easy for me to add additional validation or checks to the
data. For example, if I wanted to check that the data returned from the server
was not empty I could do something like this:
Domain Layer
The domain layer is responsible for defining the types that are used in the
application. In this example we are using io-ts to define the types. This
allows us to define the types and also validate the data at runtime. This is
very useful when working with data from external sources.
My goal with a domain layer is always to define exactly what is needed in the
application and nothing more. This means that I can be very specific about the
data that is required and also the data that is returned. This makes it very
easy to reason about the application and also makes it very easy to test.
For errors, I've kept it simple and just defined a few different types of errors
that can occur. This is not a complete list of errors that can occur but it
should be enough to demonstrate the concept.
I want to catch and handle any exceptions that occur in the application. I don't
want to have to worry about exceptions being thrown in the application. I want
to be able to handle them in a consistent way. This is why I have defined a
NetworkError and a ParseError. These errors are thrown by the
fetchProducts function and are handled in the application layer.
Application Layer
The application layer is responsible for defining the business logic of the
application. This is where we define the functions that are used to update the
state of the application. In this example we are using fp-ts to define the
functions. This allows us to compose the functions and handle any errors that
may occur.
The useShoppingCart hook is responsible for fetching the data and updating the
state of the application. It is also responsible for displaying any errors that
occur. This is where we use the pipe function to compose the functions and
handle any errors that may occur.
Storing the state of the application in a useState hook is not ideal. It would
be better to use a state management library like redux or zustand. I have
used useState here to keep the example simple.
Presentation Layer
The presentation layer is responsible for displaying the data to the user. In
this example we are using react to display the data. The ShoppingApp
component is responsible for displaying the data to the user. It is also
responsible for handeling the state of the application. This is where we use the
pipe function to compose the functions and handle any errors, or loading that
may occur.
Since the ShoppingApp component is responsible for seperating the states of
the application, I don't need to worry about errors or loading in the ShopCard
component. I can just focus on displaying the data to the user in each state.
The ProductCard and CartItem components are responsible for displaying the
data to the user. Since I've already delt with errors and loading in the
ShoppingApp component I don't need to worry about them here. I can just focus
on displaying the data to the user.