A Simple Key-Value Store with Servant
The meat of the Servant tutorial starts with an imposing list of language extensions and imports and only gets more confusing from there. I don’t think this gives newbies (i.e. me) the best first impression of Servant. Let’s build the simplest possible key-value store with it instead.
We’re going to write this as a stack
script so everything is in one file.
#!/usr/bin/env stack
Let’s import the modules we need.
{- stack --resolver lts-7 --install-ghc runghc
--package aeson
--package servant-server
--package text
--package transformers
--package unordered-containers
--package warp
-}
At minimum we need servant-server
for Servant goodness and warp
to actually
run our web service. The plan is to create an IORef
holding a HashMap
and
use that as our store, which is why we need unordered-containers
. I’d like to
store arbitrary JSON, therefore aeson
, and I think our keys should be Text
,
because aeson
and Text
are great together. That leaves transformers
,
which we need because of liftIO
.
It turns out that we only need two language extensions for this example.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
As far as I can tell, both these extensions are needed for Servant’s cute API specification EDSL. We’ll let you have this one, Servant.
Time for a (hopefully manageable) list of imports!
module Main where
import Control.Monad.IO.Class (liftIO)
import Data.Aeson (Value)
import Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef')
import Data.HashMap.Strict (HashMap, lookup, insert, empty)
import Data.Text (Text)
import Network.Wai.Handler.Warp (run)
import System.Environment (getArgs)
import Prelude hiding (lookup)
import Servant
All imports are explicit except Servant
’s.
Speaking of the EDSL:
type API
= "get" :> Capture "key" Text :> Get '[JSON] (Maybe Value)
:<|> "put" :> Capture "key" Text
:> ReqBody '[JSON] Value :> Put '[JSON] Text
This API has two endpoints: a “/get/:key” endpoint that provides the value associated with a key if the key exists in our store, or a “put/:key” endpoint that allows us to associate some JSON with a key, returning the key used. How this fits together is still a bit magical to me, but this blog post provides the best explanation I’ve read so far. The section of the Servant tutorial on the API specification EDSL is also quite good.
Let’s define a type synonym so we don’t have to keep writing IORef (HashMap Text Value)
over and over again:
type Store = IORef (HashMap Text Value)
Servant uses the same operator to define the type and the serving action:
server :: Store -> Server API
server store = getValue store :<|> putValue store
The order in which these actions are composed needs to match the order used in the API definition.
Next we define getValue
and putValue
. We need actions of type Handler
,
which is ExceptT ServantErr IO
, so we liftIO
as necessary:
getValue :: Store -> Text -> Handler (Maybe Value)
getValue store key = liftIO $ lookup key <$> readIORef store
putValue :: Store -> Text -> Value -> Handler Text
putValue store key value = liftIO $ atomicModifyIORef' store modify
where modify kv = (insert key value kv, key)
Almost there. We declare the API we want to serve:
kvAPI :: Proxy API
kvAPI = Proxy
Once again, Kwang Yul Seo has a great blog post on this.
Finally, we define our entry point:
main :: IO ()
main = do
port <- read . head <$> getArgs :: IO Int
run port . serve kvAPI . server =<< newIORef empty
To recap: we define our API as a type, our handlers, and the type we want to serve, and then we plug it all together.
And we’re done! We can now chmod +x KVStore.hs
(or whatever you called the
file) and run it: ./KVStore.hs 8081
.
I hope this provides a better starting point for learning Servant. If desired, the full script is available here.