Using Selda with Servant

Posted on

I came to Haskell late last year, and of course nowadays we’re mostly working with APIs and databases. In the Haskell ecosystem, I think the API part has already been figured out. We have Servant. The first time I worked with it I talked to myself: this is how you should be building API, request in, record out, nothing else. With Servant you will not have to worry about mundane tasks like adding correct header, method checking and content negotiation, .etc, just focus on writing the logic that really matter.

The SQL part though, is still messy. There’s Persistent, which is kind of like ActiveRecord, in the sense that you have little control over your queries, people use it with a separate query builder (Esqueleto) which is not ideal. There’s also postgresql-simple which seems loved by everyone, but it is pretty low level and certainly not type safe. I’m coming to Haskell for safety, and if it is not guaranteed I would just come back to Go. Until I met Selda, it feels just right. In my opinion it is at the right level of abstraction I want to have, not too low as postgresql-simple, and also not too high as persistent or groundhog. It has a unique representation of database records as “inductive tuple”, which can trace back to the theory of relational database design.

However, Selda is relatively new and I did not find any pointer to how to use it with Servant. I started with the Servant documentations about using Postgreql, which is not very helpful because it makes the database connection a parameter of the API. We’re using Haskell, we solve problems using monads, so I started to look at Servant’s custom monad tutorial.

Long story short, following is what I’ve come up with. Let say we’re building an API for a blog, here is the API definition in Servant:

type BlogAPI = PostList :<|> Capture "slug" Text :> PostView

type PostList = Get '[JSON] [BlogPost]
type PostView = Get '[JSON] (Maybe BlogPost)

Here’s my application monad:

data Env = Env
  { dbPool :: !(Pool SeldaConnection)

To glue them together I defined an MonadSelda instance for my application monad (in this case I also used the RIO monad).

class HasConnectionPool env where
  connectionPoolL :: Lens' env (Pool SeldaConnection)

instance HasConnectionPool Env where
  connectionPoolL = lens dbPool (\env pool -> env { dbPool = pool })

instance (HasConnectionPool env) => MonadSelda (RIO env) where
  seldaConnection = do
    pool <- view connectionPoolL
    liftIO $ withResource pool pure

Then the API implementation:

main :: IO ()
main = do
  dbPool <- createPool (pgOpen' Nothing "") seldaClose 4 2 10
  serve api $ hoistServer api (runRIO Env{..}) app

api :: Proxy BlogAPI
api = Proxy

app :: ServerT BlogAPI (RIO Env)
app = getPostList :<|> getPost

getPostList :: (HasConnectionPool env) => RIO env [BlogPost]
getPostList = fmap fromRels $ do
  posts <- select tablePost
  order (posts ! pCreatedAt_) descending
  pure posts

getPost :: (HasConnectionPool env) => Text -> RIO env (Maybe BlogPost)
getPost url = fmap (listToMaybe . fromRels) $ do
  posts <- select tablePost
  restrict (posts ! pUrl_ .== text url)
  pure posts

You can see that getPostList and getPost are totally free of any details from the API, and you can compose them just like any other data fetch in other API handler. Suppose in a company we have different system with different database setup, we can just import the data fetch functions and compose them into an API with minimal effort.

In conclusion I really love the this combination. It really shows the power of Haskell type system and how language features like monad make integration seamless.