4. Server implementation¶
An ApiContract is just a schematic representation of your API service. We still need to implement our handlers that actually does the work. You would have already read about this in the Quick start section.
Implementation of a contract consists of
- Writing a WebApiServer instance.
- Writing ApiHandler instances for all your end-points.
4.1. Writing WebApiServer instance¶
WebApiServer typeclass has
- Two associated types
- HandlerM - It is the type of monad in which our handler should run (defaults to
IO). This monad should implement
- ApiInterface -
ApiInterfacelinks the implementation with the contract. This lets us have multiple implementations for the same contract
- One method
- toIO - It is a method which is used to convert our handler monad’s action to
IO. (defaults to
Let’s define a type for our implementation and write a
WebApiServer instance for the same.
data MyApiServiceImpl = MyApiServiceImpl instance WebApiServer MyApiServiceImpl where type HandlerM MyApiServiceImpl = IO type ApiInterface MyApiServiceImpl = MyApiService toIO _ = id
You can skip writing
toIO‘s definitions if
you want your
HandlerM to be
4.2. Writing instances for your handlers¶
Now we can write handler for our
User route as
instance ApiHandler MyApiServiceImpl POST User where handler _ req = do let _userInfo = formParam req respond (UserToken "Foo" "Bar")
handler returns a
Response. Here we used
You can use its counter-part
raise as discussed in Error Handling
4.3. Doing more with your handler monad¶
Though the above implementation can get you started, it falls short for many practical scenarios. We’ll discuss some of them in the following sections.
4.3.1. Adding a config Reader¶
Most of the times our app would need some kind of initial setting which could
come from a config file or some environment variables. To accomodate for that, we
data AppSettings = AppSettings data MyApiServiceImpl = MyApiServiceImpl AppSettings
AppSettings to our
MyApiServiceImpl is useless unless our
monad gives a way to access those settings. So we need a monad in which we can
read these settings, anytime we require. A
ReaderT transformer would fit
perfectly for this scenario.
For those who are not familiar with
Reader monad, it is a monad
which gives you read only access to some data(say, settings) throughout a computation.
You can access that data in your monad using
ReaderT is a
monad transformer which adds capabilities of
Reader monad on top of
another monad. In our case, we’ll add reading capabilities to
IO. So the
monad for our handler would look something like
newtype MyApiMonad a = MyApiMonad (ReaderT AppSettings IO a) deriving (Monad, MonadIO, MonadCatch)
HandlerM is required to have
instances. Thats why you see them in the
There is still one more piece left, before we can use this. We need to define
toIO function to convert
MyApiMonad‘s actions to
We can use runReaderT to pass the initial
Reader‘s environment settings
and execute the computation in the underlying monad(IO in this case).
toIO (MyApiServiceImpl settings) (MyApiMonad r) = runReaderT r settings
WebApiServer instance for our modified
would look like:
instance WebApiServer MyApiServiceImpl where type HandlerM MyApiServiceImpl = MyApiMonad type ApiInterface MyApiServiceImpl = MyAppService toIO (MyApiServiceImpl settings) (MyApiMonad r) = runReaderT r settings
ApiHandler for this would be something like:
instance ApiHandler MyApiServiceImpl POST User where handler _ req = do settings <- ask -- do something with settings respond (UserToken "Foo" "Bar")
4.3.2. Adding a logger¶
Adding a logging system to our implementation is similar to adding a
LoggingT transformer to achieve that.
newtype MyApiMonad a = MyApiMonad (LoggingT (ReaderT AppSettings IO) a) deriving (Monad, MonadIO, MonadCatch, MonadLogger) instance WebApiServer MyApiServiceImpl where type HandlerM MyApiServiceImpl = MyApiMonad type ApiInterface MyApiServiceImpl = MyAppService toIO (MyApiServiceImpl settings) (MyApiMonad r) = runReaderT (runStdoutLoggingT r) settings