haskelldependency-injectionservant

Reading file contents once at haskell application startup


I have an API written using the Servant library which connects to a postgres db. The connection string for my db is stored in a configuration file. Everytime I make a request to any endpoint that interacts with the db I have to read the file to get the connection string, this is what im trying to avoid.

Step by step example of what im trying to achieve:

  1. Application starts up.
  2. Contents of the config file are read and bound to some type/object.
  3. I make a request to my endpoint to create an entry in the db.
  4. I read the connection string from the type/object that I bound it to and NOT the config file.
  5. Every subsequent request for the lifetime of the application does not have to read the config file everytime it wants to interact with the database.

Doing this in something like java/c# you would just bind the contents of a file to some POCO which would be added to your DI container as a singleton so it can be referenced anywhere in your application and persist between each api request. If I have 100 requests that ineract with the db, none of those 100 requests would need to read config file to get the connection string as it was already loaded into memory when the app started.

I have thought about using the cache package, but is there an easier way to do something like this without a third party package?


Solution

  • Let's begin with this trivial Servant server:

    import Servant
    import Servant.Server
    
    type FooAPI = Get '[JSON] Int
    
    fooServer :: Server FooAPI
    fooServer = pure 1
    

    Suppose we don't want to hardcode that 1. We could turn fooServer into a function like

    makeFooServer :: Int -> Server FooAPI
    makeFooServer n = pure n
    

    Then, in main, we could read the n from a file then call makeFooServer to construct the server. Something similar could be done for your database connection.


    There's another approach that might be sometimes preferrable. Servant lets you define servers whose handlers live in a monad different from Handler, and then transform them into regular servers (tutorial).

    We can write a server in which the handler monad is a ReaderT holding the configuration:

    import Control.Monad.Trans.Reader
    
    type RHandler env = ReaderT env Handler
    
    fooServer' :: ServerT FooAPI (RHandler Int)
    fooServer' = do 
      n <- ask
      pure n
    

    Where ServerT is a more general form of Server that lets you specify the handler monad in an extra type argument.

    Then, we use the hoistServer function to supply the initial environment and go back to a regular server:

    -- "Server FooAPI" is the same as "ServerT FooAPI Handler"
    -- so the transformation we need is simply to run the `ReaderT`
    -- by supplying an environment.
    backToNormalServer :: Int -> Server FooAPI
    backToNormalServer n = hoistServer (Proxy @FooAPI) (flip runReaderT n) fooServer'
    

    The ServerT FooAPI (RHandler Int) approach has the advantage that you still have a server value that you can directly manipulate and pass around, instead of it being the result of a function.

    Also, for some advanced use cases, the environment might reflect information derived from the structure of each endpoint.