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¶
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 implementMonadCatch
andMonadIO
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 toid
)
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