Serving HTML with Servant
We now have several different ways of generating HTML code from Haskell. Our last look at this issue explored the Lucid library. But in most cases you won't be writing client side Haskell code.
You'll have to send the HTML you generate to your end-user, typically over a web server. So in this article we're going to explore the most basic way we can do that. We'll see how we can use the Servant library to send HTML in response to API requests.
For a more in-depth tutorial on making a web app with Servant, read our Real World Haskell series! You can also get some more ideas for Haskell libraries in our Production Checklist.
Servant Refresher
Suppose we have a basic User
type, along with JSON instances for it:
data User = User
{ userId :: Int
, userName :: String
, userEmail :: String
, userAge :: Int
}
instance FromJSON User where
…
instance ToJSON User where
...
In Servant, we can expose an endpoint to retrieve a user by their database ID. We would have this type in our API definition, and a handler function.
type MyAPI =
"users" :> Capture "uid" Int :> Get '[JSON] (Maybe User) :<|>
... -- other endpoints
userHandler :: Int -> Handler (Maybe User)
…
myServer :: Server MyAPI
myServer =
userHandler :<|>
... -- Other handlers
Our endpoint says that when we get a request to /users/:uid
, we'll return a User
object, encoded in JSON. The userHandler
performs the logic of retrieving this user from our database.
We would then let client side Javascript code actually to the job of rendering our user as HTML. But let's flip the script a bit and embrace the idea of "server side rendering." Here, we'll gather the user information and generate HTML on our server. Then we'll send the HTML back in reply. First, we'll need a couple pieces of boilerplate.
A New Content Type
In the endpoint above, the type list '[JSON]
refers to the content type of our output. Servant knows when we have JSON
in our list, it should include a header in the response indicating that it's in JSON.
We now want to make a content type for returning HTML. Servant doesn't have this by default. If we try to return a PlainText
HTML string, the browser won't render it! It will display the raw HTML string on a blank page!
So to make this work, we'll start with two types. The first will be HTML
. This will be our equivalent to JSON
, and it's a dummy type, with no actual data! The second will be RawHtml
, a simple wrapper for an HTML bytestring.
import Data.ByteString.Lazy as Lazy
data HTML = HTML
newtype RawHtml = RawHtml { unRaw :: Lazy.ByteString }
We'll use the HTML
type in our endpoints as we currently do with JSON
. It's a content type, and our responses need to know how to render it. This means making an instance of the Accept
class. Using some helpers, we'll make this instance use the content-type: text/html
header.
import Network.HTTP.Media ((//), (/:))
import Servant.API (Accept(..))
instance Accept HTML where
contenType _ = "text" // "html" /: ("charset", "utf-8")
Then, we'll link our RawHtml
type to this HTML
class with the MimeRender
class. We just unwrap the raw bytestring to send in the response.
instance MimeRender HTML RawHtml where
mimeRender _ = unRaw
This will let us use the combination of HTML
content type and RawHtml
result type in our endpoints, as we'll see. This is like making a ToJSON
instance for a different type to use with the JSON
content type.
An HTML Endpoint
Now we can rewrite our endpoint so that it returns HTML instead! First we'll make a function that renders our User
. We'll use Lucid in this case:
import Lucid
renderUser :: Maybe User -> Html ()
renderUser maybeUser = html_ $ do
head_ $ do
title_ "User Page"
link_ [rel_ "stylesheet", type_ "text/css", href_ "/styles.css"]
body_ $ userBody
where
userBody = case maybeUser of
Nothing -> div_ [class_ "login-message"] $ do
p_ "You aren't logged in!"
br_ []
a_ [href_ "/login"] "Please login"
Just u -> div_ [class_ "user-message"] $ do
p_ $ toHtml ("Name: " ++ userName u)
p_ $ toHtml ("Email: " ++ userEmail u)
p_ $ toHtml ("Age: " ++ show (userAge u))
Now we'll need to re-write our endpoint, so it uses our new type:
type MyAPI = "users" :> Capture "uid" Int :> Get '[HTML] RawHtml :<|>
...
Finally, we would rewrite our handler function to render the user immediately!
userHandler :: Int -> Handler RawHtml
userHandler uid = do
maybeUser <- fetchUser uid -- DB lookup or something
return (RawHtml $ renderHtml (renderUser maybeUser))
Our server would now work, returning the HTML string, which the browser would render!
Serving Static Files
There's one more thing we need to handle! Remember that HTML by itself is not typically enough. Our HTML files almost always reference other files, like CSS, Javascript, and images. When the user loads the HTML we send, they'll make another immediate request for those files. As is, our server won't render any styles for our user HTML. How do we serve these?
In Servant, the answer is the serveDirectoryWebApp
function. This allows us to serve out the files from a particular file as static files. The first piece of this puzzle is to add an extra endpoint to our server definition. This will catch all patterns and return a Raw
result. This means the contents of a particular file.
type MyAPI =
"users" :> Capture "uid" Int :> Get '[HTML] RawHtml :<|>
Raw
This endpoint must come last out of all our endpoints, even if we compose MyAPI
with other API types. Otherwise it will catch every request and prevent other handlers from operating! This is like when you use a catch-all too early in a case
statement.
Now for our "handler", we'll use the special serve
function.
myServer :: Server MyAPI
myServer =
userHandler <|>:
serveDirectoryWebApp "static"
And now, if we have styles.css
with appropriate styles, they'll render correctly!
Conclusion
It's a useful exercise to go through the process of making our HTML
content type manually. But Blaze and Lucid both have their own helper libraries to simplify this. Take a look at servant-blaze and servant-lucid. You can import the corresponding modules and this will handle the boilerplate for you.
Next week, we'll explore a few extra things we can do with Servant. We'll see some neat combinators that allow us to test our Servant API with ease!
Don't forget you can take a look at our Github repository for more details! This week's code is in `src/BasicServant.hs.
And also remember to subscribe to Monday Morning Haskell! You'll get our monthly newsletter and access to our subscriber resources!