I am building a web API with Servant and Persistent. I plan to define some API endpoints (about 15) that use a connection pool to access the DB.
For example, one of the endpoint definitions (Handler
s) is:
getUser :: ConnectionPool -> Int -> Handler User
getUser pool uid = do
user <- inPool pool $ get (toId @User uid)
user & orErr err404 {errBody = "This user does not exist."}
where inPool
is just a lifted withResource
function, and orErr
is a lifted fromMaybe
.
Then, a higher level API definition (Server
s) looks like this:
type Point (s :: Symbol) (a :: *) =
s :> Capture "id" Int :>
( Get '[JSON] a
:<|> ReqBody '[JSON] a :> Post '[JSON] NoContent
)
type UserPoint = Point "users" User
userServer :: ConnectionPool -> Server UserPoint
userServer pool uid =
getUser pool uid :<|>
postUser pool uid
And I defined the main
to be:
main = runStdoutLoggingT . withPostgresqlPool connectionString numConnections $ \pool -> do
withResource pool (runSqlConn $ runMigration migrateAll)
liftIO $ run appPort (userServer pool)
But I soon noticed that I would have to pass the pool down layer by layer (In the example above there are 2 layers, and in my real project there are 3), to every function (that is over 20). My intuition tells me this is bad smell, but I am not quite sure.
Then I thought of ReaderT
, because I think that may abstract the pool out. But my concern is that the introduction of ReaderT
may lead to unnecessary complexity:
- I need to lift many things manually;
- The mental model of types will become more complicated thus harder to think about;
- It means I'll have to give up the
Handler
type, which makes using Servant harder too.
I am not sure whether I should use ReaderT
in this case. Please offer some suggestions (I'll be grateful if you could also provide some guidelines about when to use ReaderT
or even other monad transformers).
UPDATE: I found that I can use where
-clauses to simplify this a lot, and this basically solves my problem. but I'm not sure if this is best practice, so I'm still looking forwand to an answer.
userServer :: Pooled (Server UserPoint)
userServer pool auth = c :<|> rud where
c :: UserCreation -> Handler NoContent
c = undefined
rud uid = r :<|> u :<|> d where
r :: Handler User
r = do
checkAuth pool auth
user <- inPool pool $ get (toId @User uid)
user & orErr err404 {errBody = "This user does not exist."}
u :: User -> Handler NoContent
u = undefined
d :: Handler NoContent
d = undefined
StateT
? – daylilywhere
approach for avoiding having to pass the pool argument is that it forces you to define all handlers insideuserServer
. One question: when you write "pass the pool down layer by layer ", do you mean layers of function invocations? – danidiazuserServer
is not yet a very big problem to me; and for your question, yes. – daylily