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

4.1. Writing WebApiServer instance

The 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 MonadCatch and MonadIO classes.
    • ApiInterface - ApiInterface links 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 id)

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

Note

You can skip writing HandlerM‘s and toIO‘s definitions if you want your HandlerM to be IO.

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 respond to build a Success Response. You can use its counter-part raise as discussed in Error Handling to send Failure Response

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 can change MyApiServiceImpl to

data AppSettings = AppSettings

data MyApiServiceImpl = MyApiServiceImpl AppSettings

Just adding 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 ask. 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)

Note: HandlerM is required to have MonadIO and MonadCatch instances. Thats why you see them in the deriving clause.

There is still one more piece left, before we can use this. We need to define toIO function to convert MyApiMonad‘s actions to IO. 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

So the WebApiServer instance for our modified MyApiServiceImpl would look like:

instance WebApiServer MyApiServiceImpl where
    type HandlerM MyApiServiceImpl = MyApiMonad
    type ApiInterface MyApiServiceImpl = MyAppService
    toIO (MyApiServiceImpl settings) (MyApiMonad r) = runReaderT r settings

A sample 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 Reader. We use 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