If you’re a web programmer, Haskell has a lot of neat toys. Servant does a fantastic job of describing RESTful APIs, and the ability to generate JavaScript clients, Haskell clients, and Swagger documentation and UIs make it a compelling choice for implementing your API. Yesod makes a similarly compelling choice for full blown websites, with lots of documentation, HTML templates, solid routing and type safe links, and convenient database modeling. Where Servant excels for RESTful APIs, Yesod excels for websites.
If you’re writing a web app, you may be wondering: how do I choose? An API may end up needing to render some pages, and a website may need to expose JSON endpoints. Fortunately, we can easily have both! In this blog post, I’ll demonstrate how to mount my servant-persistent starter project inside of a newly minted Yesod application.
tl;dr: Both Servant and Yesod expose functions to convert them to WAI applications, and both have means of running arbitrary WAI
applications.
If you’re too impatient to read the walkthrough, the complete repository is on Github.
servant-persistent
Since we’ll be using this package as our API, you may want to read a bit about it. I wrote a post describing the project and how it’s used here.
Yesod has a feature called subsites.
This allows you to write a modular little website, and then put it inside of a larger site.
A lesser known feature is the WaiSubsite
which allows you to embed an arbitrary WAI
Application
.
We’ll use this feature to embed the Servant app.
Start up a new Yesod project using stack
, like so:
stack new yo-dawg yesod-postgres --resolver=lts-6.27
Next up, we’ll add servant-persistent
as another package in the stack.yaml
so that we can use it:
# in stack.yaml
packages:
- '.'
- location:
git: git@github.com:parsonsmatt/servant-persistent
commit: 98479a423609794ffa9b668b0ae13ae9a57be18e
extra-dep: true
And we’ll need to add servant-persistent
as a dependency of our project:
-- in yo-dawg.cabal
build-depends:
-- .......
, servant-persistent
That should be all the setup we need to do in order to start using the stuff.
Yesod comes with a bunch of stuff for models.
But servant-persistent
already has database stuff and models defined.
How should we handle this?
At the day job, I factored the models out into their own package. That’s an option that has worked well, though it’s a little more labor intensive.
You could allow the API and the website to have separate models, though that seems like a lot of duplication and shared concerns.
What we’ll do, for simplicity, is rely on the models present in the servant-persistent
app and delete the model code out of the Yesod repository.
In order to get this running, we’ll need to delete the stuff relating to Comment
s, as they’re not present in the servant-persistent
models.
We’ll also need to delete the authentication code and userIdent
references.
Thankfully, GitHub makes these changes easy to see. Here’s a commit link that shows the changes to the base template necessary.
Our next task is to put the API somewhere.
api
is a sensible place to put it, so let’s add that to the routes file.
-- in config/routes
/api ServantPersistentR WaiSubsite getServantPersistent
{-
[1] [2] [3] [4]
-}
The line has four components:
When this code is added, we’ll get an error in Foundation.hs
, since it’ll be trying to refer to getServantPersistent
, which isn’t defined.
The expected type of getServantPersistent
is going to be a function that takes our App
type in the Foundation
and returns a WaiSubsite
.
So we’ll add the bare minimum to shut GHC up:
-- in Foundation.hs
getServantPersistent :: App -> WaiSubsite
getServantPersistent = error "later"
Now, compilation succeeds.
The next step is to use the WaiSubsite
constructor, which has the type Application -> WaiSubsite
, where Application
is a WAI application.
-- in Foundation.hs
getServantPersistent :: App -> WaiSubsite
getServantPersistent = WaiSubsite . error "later"
Now, we’re left with the question: Given our App
, how do we get the servant-persistent
Application
out of it?
servant-persistent
has a Config
type that is in many ways similar to the App
type in Yesod.
It contains all of the Stuff you need in order to get the API up and running, including the settings, database pool, etc.
Fortunately, the package also exposes a function app :: Config -> Application
.
So all we need to do is get our hands on the Config
data type, call that function, and we’re set.
Fortunately, in this case, the Config
is pretty simple: just an Environment
and a ConnectionPool
.
Yesod prefers to handle Environment
by different (and better) means, so we’ll just pass Production
in.
The ConnectionPool
is created in Application.hs
function makeFoundation
.
Since we don’t want to re-make the API Application on every request, we’ll just go ahead and add the Application
to the Yesod App
data type.
In Foundation.hs
, we’ll make the following changes:
data App = App
{ appSettings :: AppSettings
, appStatic :: Static -- ^ Settings for static file serving.
, appConnPool :: ConnectionPool -- ^ Database connection pool.
, appHttpManager :: Manager
, appLogger :: Logger
-- vvv New! :D vvv
, appSubApi :: Application
-- ^^^ New! :D ^^^
}
getServantPersistent :: App -> WaiSubsite
getServantPersistent = WaiSubsite . appSubApi
And in Application.hs
, we’ll need to initialize the API when we get the connection pool.
Yesod does a bit of a hack by default here, so we’ll respond in kind with a hack.
In the function makeFoundation
, we’ll modify the mkFoundation
function defined in the let
like so:
-- Application.hs
import qualified Api as ServantPersistent
import Config (Config(..), Environment(Production))
-- down to makeFoundation ...
let mkFoundation appConnPool =
let apiCfg = Config appConnPool Production
appSubApi = ServantPersistent.app apiCfg
in App {..}
-- ...
This ties everything together, and you’ll be serving your Servant API out of a Yesod subsite. Neat!