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.
Let’s import the modules we need.
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 are great together. That leaves
transformers, which we need because of
It turns out that we only need two language extensions for this example.
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
Speaking of the EDSL:
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:
Servant uses the same operator to define the type and the serving action:
The order in which these actions are composed needs to match the order used in the API definition.
Next we define
putValue. We need actions of type
Handler, which is
ExceptT ServantErr IO, so we
liftIO as necessary:
Almost there. We declare the API we want to serve:
Once again, Kwang Yul Seo has a great blog post on this.
Finally, we define our entry point:
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:
I hope this provides a better starting point for learning Servant. If desired, the full script is available here.