Yesod for Haskellers

The majority of this book is built around giving practical information on how to get common tasks done, without drilling too much into the details of what’s going on under the surface. While the book presumes knowledge of Haskell, it does not follow the typical style of many Haskell libraries introductions. Many seasoned Haskellers are put off by this hiding of implementation details. The purpose of this chapter is to address those concerns.

In this chapter, we’ll start off from a bare minimum web application, and build up to more complicated examples, explaining the components and their types along the way.

Hello Warp

Let’s start off with the most bare minimum application we can think of:

  1. {-# LANGUAGE OverloadedStrings #-}
  2. import Network.HTTP.Types (status200)
  3. import Network.Wai (Application, responseLBS)
  4. import Network.Wai.Handler.Warp (run)
  5. main :: IO ()
  6. main = run 3000 app
  7. app :: Application
  8. app _req sendResponse = sendResponse $ responseLBS
  9. status200
  10. [("Content-Type", "text/plain")]
  11. "Hello Warp!"

Wait a minute, there’s no Yesod in there! Don’t worry, we’ll get there. Remember, we’re building from the ground up, and in Yesod, the ground floor is WAI, the Web Application Interface. WAI sits between a web handler, such as a web server or a test framework, and a web application. In our case, the handler is Warp, a high performance web server, and our application is the app function.

What’s this mysterious Application type? It’s a type synonym defined as:

  1. type Application = Request
  2. -> (Response -> IO ResponseReceived)
  3. -> IO ResponseReceived

The Request value contains information such as the requested path, query string, request headers, request body, and the IP address of the client. The second argument is the “send response” function. Instead of simply having the application return an IO Response, WAI uses continuation passing style to allow for full exception safety, similar to how the bracket function works.

We can use this to do some simple dispatching:

  1. {-# LANGUAGE OverloadedStrings #-}
  2. import Network.HTTP.Types (status200)
  3. import Network.Wai (Application, pathInfo, responseLBS)
  4. import Network.Wai.Handler.Warp (run)
  5. main :: IO ()
  6. main = run 3000 app
  7. app :: Application
  8. app req sendResponse =
  9. case pathInfo req of
  10. ["foo", "bar"] -> sendResponse $ responseLBS
  11. status200
  12. [("Content-Type", "text/plain")]
  13. "You requested /foo/bar"
  14. _ -> sendResponse $ responseLBS
  15. status200
  16. [("Content-Type", "text/plain")]
  17. "You requested something else"

WAI mandates that the path be split into individual fragments (the stuff between forward slashes) and converted into text. This allows for easy pattern matching. If you need the original, unmodified ByteString, you can use rawPathInfo. For more information on the available fields, please see the WAI Haddocks.

That addresses the request side; what about responses? We’ve already seen responseLBS, which is a convenient way of creating a response from a lazy ByteString. That function takes three arguments: the status code, a list of response headers (as key/value pairs), and the body itself. But responseLBS is just a convenience wrapper. Under the surface, WAI uses blaze-builder’s Builder data type to represent the raw bytes. Let’s dig down another level and use that directly:

  1. {-# LANGUAGE OverloadedStrings #-}
  2. import Blaze.ByteString.Builder (Builder, fromByteString)
  3. import Network.HTTP.Types (status200)
  4. import Network.Wai (Application, responseBuilder)
  5. import Network.Wai.Handler.Warp (run)
  6. main :: IO ()
  7. main = run 3000 app
  8. app :: Application
  9. app _req sendResponse = sendResponse $ responseBuilder
  10. status200
  11. [("Content-Type", "text/plain")]
  12. (fromByteString "Hello from blaze-builder!" :: Builder)

This opens up some nice opportunities for efficiently building up response bodies, since Builder allows for O(1) append operations. We’re also able to take advantage of blaze-html, which sits on top of blaze-builder. Let’s see our first HTML application.

  1. {-# LANGUAGE OverloadedStrings #-}
  2. import Network.HTTP.Types (status200)
  3. import Network.Wai (Application, responseBuilder)
  4. import Network.Wai.Handler.Warp (run)
  5. import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
  6. import Text.Blaze.Html5 (Html, docTypeHtml)
  7. import qualified Text.Blaze.Html5 as H
  8. main :: IO ()
  9. main = run 3000 app
  10. app :: Application
  11. app _req sendResponse = sendResponse $ responseBuilder
  12. status200
  13. [("Content-Type", "text/html")] -- yay!
  14. (renderHtmlBuilder myPage)
  15. myPage :: Html
  16. myPage = docTypeHtml $ do
  17. H.head $ do
  18. H.title "Hello from blaze-html and Warp"
  19. H.body $ do
  20. H.h1 "Hello from blaze-html and Warp"

But there’s a limitation with using a pure Builder value: we need to create the entire response body before returning the Response value. With lazy evaluation, that’s not as bad as it sounds, since not all of the body will live in memory at once. However, if we need to perform some I/O to generate our response body (such as reading data from a database), we’ll be in trouble.

To deal with that situation, WAI provides a means for generating streaming response bodies. It also allows explicit control of flushing the stream. Let’s see how this works.

  1. {-# LANGUAGE OverloadedStrings #-}
  2. import Blaze.ByteString.Builder (Builder, fromByteString)
  3. import Blaze.ByteString.Builder.Char.Utf8 (fromShow)
  4. import Control.Concurrent (threadDelay)
  5. import Control.Monad (forM_)
  6. import Control.Monad.Trans.Class (lift)
  7. import Data.Monoid ((<>))
  8. import Network.HTTP.Types (status200)
  9. import Network.Wai (Application,
  10. responseStream)
  11. import Network.Wai.Handler.Warp (run)
  12. main :: IO ()
  13. main = run 3000 app
  14. app :: Application
  15. app _req sendResponse = sendResponse $ responseStream
  16. status200
  17. [("Content-Type", "text/plain")]
  18. myStream
  19. myStream :: (Builder -> IO ()) -> IO () -> IO ()
  20. myStream send flush = do
  21. send $ fromByteString "Starting streaming response.\n"
  22. send $ fromByteString "Performing some I/O.\n"
  23. flush
  24. -- pretend we're performing some I/O
  25. threadDelay 1000000
  26. send $ fromByteString "I/O performed, here are some results.\n"
  27. forM_ [1..50 :: Int] $ \i -> do
  28. send $ fromByteString "Got the value: " <>
  29. fromShow i <>
  30. fromByteString "\n"

wai previously relied on the conduit library for its streaming data abstraction, but has since gotten rid of that dependency. However, conduit is still well supported in the WAI ecosystem, via the wai-conduit helper package.

Another common requirement when dealing with a streaming response is safely allocating a scarce resource- such as a file handle. By safely, I mean ensuring that the resource will be released, even in the case of some exception. This is where the continuation passing style mentioned above comes into play:

  1. {-# LANGUAGE OverloadedStrings #-}
  2. import Blaze.ByteString.Builder (fromByteString)
  3. import qualified Data.ByteString as S
  4. import Data.Conduit (Flush (Chunk), ($=))
  5. import Data.Conduit.Binary (sourceHandle)
  6. import qualified Data.Conduit.List as CL
  7. import Network.HTTP.Types (status200)
  8. import Network.Wai (Application, responseStream)
  9. import Network.Wai.Handler.Warp (run)
  10. import System.IO (IOMode (ReadMode), withFile)
  11. main :: IO ()
  12. main = run 3000 app
  13. app :: Application
  14. app _req sendResponse = withFile "index.html" ReadMode $ \handle ->
  15. sendResponse $ responseStream
  16. status200
  17. [("Content-Type", "text/html")]
  18. $ \send _flush ->
  19. let loop = do
  20. bs <- S.hGet handle 4096
  21. if S.null bs
  22. then return ()
  23. else send (fromByteString bs) >> loop
  24. in loop

Notice how we’re able to take advantage of existing exception safe functions like withFile to deal with exceptions properly.

But in the case of serving files, it’s more efficient to use responseFile, which can use the sendfile system call to avoid unnecessary buffer copies:

  1. {-# LANGUAGE OverloadedStrings #-}
  2. import Network.HTTP.Types (status200)
  3. import Network.Wai (Application, responseFile)
  4. import Network.Wai.Handler.Warp (run)
  5. main :: IO ()
  6. main = run 3000 app
  7. app :: Application
  8. app _req sendResponse = sendResponse $ responseFile
  9. status200
  10. [("Content-Type", "text/html")]
  11. "index.html"
  12. Nothing -- means "serve whole file"
  13. -- you can also serve specific ranges in the file

There are many aspects of WAI we haven’t covered here. One important topic is WAI middlewares. We also haven’t inspected request bodies at all. But for the purposes of understanding Yesod, we’ve covered enough for the moment.

What about Yesod?

In all our excitement about WAI and Warp, we still haven’t seen anything about Yesod! Since we just learnt all about WAI, our first question should be: how does Yesod interact with WAI. The answer to that is one very important function:

  1. toWaiApp :: YesodDispatch site => site -> IO Application

There’s an even more basic function in Yesod, called toWaiAppPlain. The distinction is that toWaiAppPlain doesn’t install any additional WAI middlewares, while toWaiApp provides commonly used middlewares, such as logging, GZIP compression, and HEAD request method handling.

This function takes some site value, which must be an instance of YesodDispatch, and creates an Application. This function lives in the IO monad, since it will likely perform actions like allocating a shared logging buffer. The more interesting question is what this site value is all about.

Yesod has a concept of a foundation data type. This is a data type at the core of each application, and is used in three important ways:

  • It can hold onto values that are initialized and shared amongst all aspects of your application, such as an HTTP connection manager, a database connection pool, settings loaded from a file, or some shared mutable state like a counter or cache.

  • Typeclass instances provide even more information about your application. The Yesod typeclass has various settings, such as what the default template of your app should be, or the maximum allowed request body size. The YesodDispatch class indicates how incoming requests should be dispatched to handler functions. And there are a number of typeclasses commonly used in Yesod helper libraries, such as RenderMessage for i18n support or YesodJquery for providing the shared location of the jQuery Javascript library.

  • Associated types (i.e., type families) are used to create a related route data type for each application. This is a simple ADT that represents all legal routes in your application. But using this intermediate data type instead of dealing directly with strings, Yesod applications can take advantage of the compiler to prevent creating invalid links. This feature is known as type safe URLs.

In keeping with the spirit of this chapter, we’re going to create our first Yesod application the hard way, by writing everything manually. We’ll progressively add more convenience helpers on top as we go along.

  1. {-# LANGUAGE OverloadedStrings #-}
  2. {-# LANGUAGE TypeFamilies #-}
  3. import Network.HTTP.Types (status200)
  4. import Network.Wai (responseBuilder)
  5. import Network.Wai.Handler.Warp (run)
  6. import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
  7. import qualified Text.Blaze.Html5 as H
  8. import Yesod.Core (Html, RenderRoute (..), Yesod,
  9. YesodDispatch (..), toWaiApp)
  10. import Yesod.Core.Types (YesodRunnerEnv (..))
  11. -- | Our foundation datatype.
  12. data App = App
  13. { welcomeMessage :: !Html
  14. }
  15. instance Yesod App
  16. instance RenderRoute App where
  17. data Route App = HomeR -- just one accepted URL
  18. deriving (Show, Read, Eq, Ord)
  19. renderRoute HomeR = ( [] -- empty path info, means "/"
  20. , [] -- empty query string
  21. )
  22. instance YesodDispatch App where
  23. yesodDispatch (YesodRunnerEnv _logger site _sessionBackend _ _) _req sendResponse =
  24. sendResponse $ responseBuilder
  25. status200
  26. [("Content-Type", "text/html")]
  27. (renderHtmlBuilder $ welcomeMessage site)
  28. main :: IO ()
  29. main = do
  30. -- We could get this message from a file instead if we wanted.
  31. let welcome = H.p "Welcome to Yesod!"
  32. waiApp <- toWaiApp App
  33. { welcomeMessage = welcome
  34. }
  35. run 3000 waiApp

OK, we’ve added quite a few new pieces here, let’s attack them one at a time. The first thing we’ve done is created a new datatype, App. This is commonly used as the foundation data type name for each application, though you’re free to use whatever name you want. We’ve added one field to this datatype, welcomeMessage, which will hold the content for our homepage.

Next we declare our Yesod instance. We just use the default values for all of the methods for this example. More interesting is the RenderRoute typeclass. This is the heart of type-safe URLs. We create an associated data type for App which lists all of our app’s accepted routes. In this case, we have just one: the homepage, which we call HomeR. It’s yet another Yesod naming convention to append R to all of the route data constructors.

We also need to create a renderRoute method, which converts each type-safe route value into a tuple of path pieces and query string parameters. We’ll get to more interesting examples later, but for now, our homepage has an empty list for both of those.

YesodDispatch determines how our application behaves. It has one method, yesodDispatch, of type:

  1. yesodDispatch :: YesodRunnerEnv site -> Application

YesodRunnerEnv provides three values: a Logger value for outputting log messages, the foundation datatype value itself, and a session backend, used for storing and retrieving information for the user’s active session. In real Yesod applications, as you’ll see shortly, you don’t need to interact with these values directly, but it’s informative to understand what’s under the surface.

The return type of yesodDispatch is Application from WAI. But as we saw earlier, Application is simply a CPSed function from Request to Response. So our implementation of yesodDispatch is able to use everything we learned about WAI above. Notice also how we accessed the welcomeMessage from our foundation data type.

Finally, we have the main function. The App value is easy to create and, as you can see, you could just as easily have performed some I/O to acquire the welcome message. We use toWaiApp to obtain a WAI application, and then pass off our application to Warp, just like we did in the past.

Congratulations, you’ve now seen your first Yesod application! (Or, at least your first Yesod application in this chapter.)

The HandlerT monad transformer

While that example was technically using Yesod, it was incredibly uninspiring. There’s no question that Yesod did nothing more than get in our way relative to WAI. And that’s because we haven’t started taking advantage of any of Yesod’s features. Let’s address that, starting with the HandlerT monad transformer.

There are many common things you’d want to do when handling a single request, e.g.:

  • Return some HTML.

  • Redirect to a different URL.

  • Return a 404 not found response.

  • Do some logging.

To encapsulate all of this common functionality, Yesod provides a HandlerT monad transformer. The vast majority of the code you write in Yesod will live in this transformer, so you should get acquainted with it. Let’s start off by replacing our previous YesodDispatch instance with a new one that takes advantage of HandlerT:

  1. {-# LANGUAGE OverloadedStrings #-}
  2. {-# LANGUAGE TypeFamilies #-}
  3. import Network.Wai (pathInfo)
  4. import Network.Wai.Handler.Warp (run)
  5. import qualified Text.Blaze.Html5 as H
  6. import Yesod.Core (HandlerT, Html, RenderRoute (..),
  7. Yesod, YesodDispatch (..), getYesod,
  8. notFound, toWaiApp, yesodRunner)
  9. -- | Our foundation datatype.
  10. data App = App
  11. { welcomeMessage :: !Html
  12. }
  13. instance Yesod App
  14. instance RenderRoute App where
  15. data Route App = HomeR -- just one accepted URL
  16. deriving (Show, Read, Eq, Ord)
  17. renderRoute HomeR = ( [] -- empty path info, means "/"
  18. , [] -- empty query string
  19. )
  20. getHomeR :: HandlerT App IO Html
  21. getHomeR = do
  22. site <- getYesod
  23. return $ welcomeMessage site
  24. instance YesodDispatch App where
  25. yesodDispatch yesodRunnerEnv req sendResponse =
  26. let maybeRoute =
  27. case pathInfo req of
  28. [] -> Just HomeR
  29. _ -> Nothing
  30. handler =
  31. case maybeRoute of
  32. Nothing -> notFound
  33. Just HomeR -> getHomeR
  34. in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
  35. main :: IO ()
  36. main = do
  37. -- We could get this message from a file instead if we wanted.
  38. let welcome = H.p "Welcome to Yesod!"
  39. waiApp <- toWaiApp App
  40. { welcomeMessage = welcome
  41. }
  42. run 3000 waiApp

getHomeR is our first handler function. (That name is yet another naming convention in the Yesod world: the lower case HTTP request method, followed by the route constructor name.) Notice its signature: HandlerT App IO Html. It’s so common to have the monad stack HandlerT App IO that most applications have a type synonym for it, type Handler = HandlerT App IO. The function is returning some Html. You might be wondering if Yesod is hard-coded to only work with Html values. We’ll explain that detail in a moment.

Our function body is short. We use the getYesod function to get the foundation data type value, and then return the welcomeMessage field. We’ll build up more interesting handlers as we continue.

The implementation of yesodDispatch is now quite different. The key to it is the yesodRunner function, which is a low-level function for converting HandlerT stacks into WAI Applications. Let’s look at its type signature:

  1. yesodRunner :: (ToTypedContent res, Yesod site)
  2. => HandlerT site IO res
  3. -> YesodRunnerEnv site
  4. -> Maybe (Route site)
  5. -> Application

We’re already familiar with YesodRunnerEnv from our previous example. As you can see in our call to yesodRunner above, we pass that value in unchanged. The Maybe (Route site) is a bit interesting, and gives us more insight into how type-safe URLs work. Until now, we only saw the rendering side of these URLs. But just as important is the parsing side: converting a requested path into a route value. In our example, this code is just a few lines, and we store the result in maybeRoute.

It’s true that our current parse function is small, but in a larger application it would need to be more complex, also dealing with issues like dynamic parameters. At that point, it becomes a non-trivial endeavor to ensure that our parsing and rendering functions remain in proper alignment. We’ll discuss how Yesod deals with that later.

Coming back to the parameters to yesodRunner: we’ve now addressed the Maybe (Route site) and YesodRunerEnv site. To get our HandlerT site IO res value, we pattern match on maybeRoute. If it’s Just HomeR, we use getHomeR. Otherwise, we use the notFound function, which is a built-in function that returns a 404 not found response, using your default site template. That template can be overridden in the Yesod typeclass; out of the box, it’s just a boring HTML page.

This almost all makes sense, except for one issue: what’s that ToTypedContent typeclass, and what does it have to do with our Html response? Let’s start by answering my question from above: no, Yesod does not in any way hard code support for Html. A handler function can return any value that has an instance of ToTypedContent. This typeclass (which we will examine in a moment) provides both a mime-type and a representation of the data that WAI can consume. yesodRunner then converts that into a WAI response, setting the Content-Type response header to the mime type, using a 200 OK status code, and sending the response body.

(To)Content, (To)TypedContent

At the very core of Yesod’s content system are the following types:

  1. data Content = ContentBuilder !Builder !(Maybe Int) -- ^ The content and optional content length.
  2. | ContentSource !(Source (ResourceT IO) (Flush Builder))
  3. | ContentFile !FilePath !(Maybe FilePart)
  4. | ContentDontEvaluate !Content
  5. type ContentType = ByteString
  6. data TypedContent = TypedContent !ContentType !Content

Content should remind you a bit of the WAI response types. ContentBuilder is similar to responseBuilder, ContentSource is like responseStream but specialized to conduit, and ContentFile is like responseFile. Unlike their WAI counterparts, none of these constructors contain information on the status code or response headers; that’s handled orthogonally in Yesod.

The one completely new constructor is ContentDontEvaluate. By default, when you create a response body in Yesod, Yesod fully evaluates the body before generating the response. The reason for this is to ensure that there are no impure exceptions in your value. Yesod wants to make sure to catch any such exceptions before starting to send your response so that, if there is an exception, Yesod can generate a proper 500 internal server error response instead of simply dying in the middle of sending a non-error response. However, performing this evaluation can cause more memory usage. Therefore, Yesod provides a means of opting out of this protection.

TypedContent is then a minor addition to Content: it includes the ContentType as well. Together with a convention that an application returns a 200 OK status unless otherwise specified, we have everything we need from the TypedContent type to create a response.

Yesod could have taken the approach of requiring users to always return TypedContent from a handler function, but that would have required manually converting to that type. Instead, Yesod uses a pair of typeclasses for this, appropriately named ToContent and ToTypedContent. They have exactly the definitions you’d expect:

  1. class ToContent a where
  2. toContent :: a -> Content
  3. class ToContent a => ToTypedContent a where
  4. toTypedContent :: a -> TypedContent

And Yesod provides instances for many common data types, including Text, Html, and aeson’s Value type (containing JSON data). That’s how the getHomeR function was able to return Html: Yesod knows how to convert it to TypedContent, and from there it can be converted into a WAI response.

HasContentType and representations

This typeclass approach allows for one other nice abstraction. For many types, the type system itself lets us know what the content-type for the content should be. For example, Html will always be served with a text/html content-type.

This isn’t true for all instance of ToTypedContent. For a counter example, consider the ToTypedContent TypedContent instance.

Some requests to a web application can be displayed with various representation. For example, a request for tabular data could be served with:

  • An HTML table

  • A CSV file

  • XML

  • JSON data to be consumed by some client-side Javascript

The HTTP spec allows a client to specify its preference of representation via the accept request header. And Yesod allows a handler function to use the selectRep/provideRep function combo to provide multiple representations, and have the framework automatically choose the appropriate one based on the client headers.

The last missing piece to make this all work is the HasContentType typeclass:

  1. class ToTypedContent a => HasContentType a where
  2. getContentType :: Monad m => m a -> ContentType

The parameter m a is just a poor man’s Proxy type. And, in hindsight, we should have used Proxy, but that would now be a breaking change. There are instances for this typeclass for most data types supported by ToTypedContent. Below is our example from above, tweaked just a bit to provide multiple representations of the data:

  1. {-# LANGUAGE OverloadedStrings #-}
  2. {-# LANGUAGE TypeFamilies #-}
  3. import Data.Text (Text)
  4. import Network.Wai (pathInfo)
  5. import Network.Wai.Handler.Warp (run)
  6. import qualified Text.Blaze.Html5 as H
  7. import Yesod.Core (HandlerT, Html, RenderRoute (..),
  8. TypedContent, Value, Yesod,
  9. YesodDispatch (..), getYesod,
  10. notFound, object, provideRep,
  11. selectRep, toWaiApp, yesodRunner,
  12. (.=))
  13. -- | Our foundation datatype.
  14. data App = App
  15. { welcomeMessageHtml :: !Html
  16. , welcomeMessageText :: !Text
  17. , welcomeMessageJson :: !Value
  18. }
  19. instance Yesod App
  20. instance RenderRoute App where
  21. data Route App = HomeR -- just one accepted URL
  22. deriving (Show, Read, Eq, Ord)
  23. renderRoute HomeR = ( [] -- empty path info, means "/"
  24. , [] -- empty query string
  25. )
  26. getHomeR :: HandlerT App IO TypedContent
  27. getHomeR = do
  28. site <- getYesod
  29. selectRep $ do
  30. provideRep $ return $ welcomeMessageHtml site
  31. provideRep $ return $ welcomeMessageText site
  32. provideRep $ return $ welcomeMessageJson site
  33. instance YesodDispatch App where
  34. yesodDispatch yesodRunnerEnv req sendResponse =
  35. let maybeRoute =
  36. case pathInfo req of
  37. [] -> Just HomeR
  38. _ -> Nothing
  39. handler =
  40. case maybeRoute of
  41. Nothing -> notFound
  42. Just HomeR -> getHomeR
  43. in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
  44. main :: IO ()
  45. main = do
  46. waiApp <- toWaiApp App
  47. { welcomeMessageHtml = H.p "Welcome to Yesod!"
  48. , welcomeMessageText = "Welcome to Yesod!"
  49. , welcomeMessageJson = object ["msg" .= ("Welcome to Yesod!" :: Text)]
  50. }
  51. run 3000 waiApp

Convenience warp function

And one minor convenience you’ll see quite a bit in the Yesod world. It’s very common to call toWaiApp to create a WAI Application, and then pass that to Warp’s run function. So Yesod provides a convenience warp wrapper function. We can replace our previous main function with the following:

  1. main :: IO ()
  2. main =
  3. warp 3000 App
  4. { welcomeMessageHtml = H.p "Welcome to Yesod!"
  5. , welcomeMessageText = "Welcome to Yesod!"
  6. , welcomeMessageJson = object ["msg" .= ("Welcome to Yesod!" :: Text)]
  7. }

There’s also a warpEnv function which reads the port number from the PORT environment variable, which is useful for working with platforms such as FP Haskell Center, or deployment tools like Keter.

Writing handlers

Since the vast majority of your application will end up living in the HandlerT monad transformer, it’s not surprising that there are quite a few functions that work in that context. HandlerT is an instance of many common typeclasses, including MonadIO, MonadTrans, MonadBaseControl, MonadLogger and MonadResource, and so can automatically take advantage of those functionalities.

In addition to that standard functionality, the following are some common categories of functions. The only requirement Yesod places on your handler functions is that, ultimately, they return a type which is an instance of ToTypedContent.

This section is just a short overview of functionality. For more information, you should either look through the Haddocks, or read the rest of this book.

Getting request parameters

There are a few pieces of information provided by the client in a request:

  • The requested path. This is usually handled by Yesod’s routing framework, and is not directly queried in a handler function.

  • Query string parameters. This can be queried using lookupGetParam.

  • Request bodies. In the case of URL encoded and multipart bodies, you can use lookupPostParam to get the request parameter. For multipart bodies, there’s also lookupFile for file parameters.

  • Request headers can be queried via lookupHeader. (And response headers can be set with addHeader.)

  • Yesod parses cookies for you automatically, and they can be queried using lookupCookie. (Cookies can be set via the setCookie function.)

  • Finally, Yesod provides a user session framework, where data can be set in a cryptographically secure session and associated with each user. This can be queried and set using the functions lookupSession, setSession and deleteSession.

While you can use these functions directly for such purposes as processing forms, you usually will want to use the yesod-form library, which provides a higher level form abstraction based on applicative functors.

Short circuiting

In some cases, you’ll want to short circuit the handling of a request. Reasons for doing this would be:

  • Send an HTTP redirect, via the redirect function. This will default to using the 303 status code. You can use redirectWith to get more control over this.

  • Return a 404 not found with notFound, or a 405 bad method via badMethod.

  • Indicate some error in the request via notAuthenticated, permissionDenied, or invalidArgs.

  • Send a special response, such as with sendFile or sendResponseStatus (to override the status 200 response code)

  • sendWaiResponse to drop down a level of abstraction, bypass some Yesod abstractions, and use WAI itself.

Streaming

So far, the examples of ToTypedContent instances I gave all involved non-streaming responses. Html, Text, and Value all get converted into a ContentBuilder constructor. As such, they cannot interleave I/O with sending data to the user. What happens if we want to perform such interleaving?

When we encountered this issue in WAI, we introduced the responseSource method of constructing a response. Using sendWaiResponse, we could reuse that same method for creating a streaming response in Yesod. But there’s also a simpler API for doing this: respondSource. respondSource takes two parameters: the content type of the response, and a Source of Flush Builder. Yesod also provides a number of convenience functions for creating that Source, such as sendChunk, sendChunkBS, and sendChunkText.

Here’s an example, which just converts our initial responseSource example from WAI to Yesod.

  1. {-# LANGUAGE OverloadedStrings #-}
  2. {-# LANGUAGE TypeFamilies #-}
  3. import Blaze.ByteString.Builder (fromByteString)
  4. import Blaze.ByteString.Builder.Char.Utf8 (fromShow)
  5. import Control.Concurrent (threadDelay)
  6. import Control.Monad (forM_)
  7. import Data.Monoid ((<>))
  8. import Network.Wai (pathInfo)
  9. import Yesod.Core (HandlerT, RenderRoute (..),
  10. TypedContent, Yesod,
  11. YesodDispatch (..), liftIO,
  12. notFound, respondSource,
  13. sendChunk, sendChunkBS,
  14. sendChunkText, sendFlush,
  15. warp, yesodRunner)
  16. -- | Our foundation datatype.
  17. data App = App
  18. instance Yesod App
  19. instance RenderRoute App where
  20. data Route App = HomeR -- just one accepted URL
  21. deriving (Show, Read, Eq, Ord)
  22. renderRoute HomeR = ( [] -- empty path info, means "/"
  23. , [] -- empty query string
  24. )
  25. getHomeR :: HandlerT App IO TypedContent
  26. getHomeR = respondSource "text/plain" $ do
  27. sendChunkBS "Starting streaming response.\n"
  28. sendChunkText "Performing some I/O.\n"
  29. sendFlush
  30. -- pretend we're performing some I/O
  31. liftIO $ threadDelay 1000000
  32. sendChunkBS "I/O performed, here are some results.\n"
  33. forM_ [1..50 :: Int] $ \i -> do
  34. sendChunk $ fromByteString "Got the value: " <>
  35. fromShow i <>
  36. fromByteString "\n"
  37. instance YesodDispatch App where
  38. yesodDispatch yesodRunnerEnv req sendResponse =
  39. let maybeRoute =
  40. case pathInfo req of
  41. [] -> Just HomeR
  42. _ -> Nothing
  43. handler =
  44. case maybeRoute of
  45. Nothing -> notFound
  46. Just HomeR -> getHomeR
  47. in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
  48. main :: IO ()
  49. main = warp 3000 App

Dynamic parameters

Now that we’ve finished our detour into the details of the HandlerT transformer, let’s get back to higher-level Yesod request processing. So far, all of our examples have dealt with a single supported request route. Let’s make this more interesting. We now want to have an application which serves Fibonacci numbers. If you make a request to /fib/5, it will return the fifth Fibonacci number. And if you visit /, it will automatically redirect you to /fib/1.

In the Yesod world, the first question to ask is: how do we model our route data type? This is pretty straight-forward: data Route App = HomeR | FibR Int. The question is: how do we want to define our RenderRoute instance? We need to convert the Int to a Text. What function should we use?

Before you answer that, realize that we’ll also need to be able to parse back a Text into an Int for dispatch purposes. So we need to make sure that we have a pair of functions with the property fromText . toText == Just. Show/Read could be a candidate for this, except that:

  1. We’d be required to convert through String.

  2. The Show/Read instances for Text and String both involve extra escaping, which we don’t want to incur.

Instead, the approach taken by Yesod is the path-pieces package, and in particular the PathPiece typeclass, defined as:

  1. class PathPiece s where
  2. fromPathPiece :: Text -> Maybe s
  3. toPathPiece :: s -> Text

Using this typeclass, we can write parse and render functions for our route datatype:

  1. instance RenderRoute App where
  2. data Route App = HomeR | FibR Int
  3. deriving (Show, Read, Eq, Ord)
  4. renderRoute HomeR = ([], [])
  5. renderRoute (FibR i) = (["fib", toPathPiece i], [])
  6. parseRoute' [] = Just HomeR
  7. parseRoute' ["fib", i] = FibR <$> fromPathPiece i
  8. parseRoute' _ = Nothing

And then we can write our YesodDispatch typeclass instance:

  1. instance YesodDispatch App where
  2. yesodDispatch yesodRunnerEnv req sendResponse =
  3. let maybeRoute = parseRoute' (pathInfo req)
  4. handler =
  5. case maybeRoute of
  6. Nothing -> notFound
  7. Just HomeR -> getHomeR
  8. Just (FibR i) -> getFibR i
  9. in yesodRunner handler yesodRunnerEnv maybeRoute req sendResponse
  10. getHomeR = redirect (FibR 1)
  11. fibs :: [Int]
  12. fibs = 0 : scanl (+) 1 fibs
  13. getFibR i = return $ show $ fibs !! i

Notice our call to redirect in getHomeR. We’re able to use the route datatype as the parameter to redirect, and Yesod takes advantage of our renderRoute function to create a textual link.

Routing with Template Haskell

Now let’s suppose we want to add a new route to our previous application. We’d have to make the following changes:

  1. Modify the Route datatype itself.

  2. Add a clause to renderRoute.

  3. Add a clause to parseRoute', and make sure it corresponds correctly to renderRoute.

  4. Add a clause to the case statement in yesodDispatch to call our handler function.

  5. Write our handler function.

That’s a lot of changes! And lots of manual, boilerplate changes means lots of potential for mistakes. Some of the mistakes can be caught by the compiler if you turn on warnings (forgetting to add a clause in renderRoute or a match in yesodDispatch‘s case statement), but others cannot (ensuring that renderRoute and parseRoute have the same logic, or adding the parseRoute clause).

This is where Template Haskell comes into the Yesod world. Instead of dealing with all of these changes manually, Yesod declares a high level routing syntax. This syntax lets you specify your route syntax, dynamic parameters, constructor names, and accepted request methods, and automatically generates parse, render, and dispatch functions.

To get an idea of how much manual coding this saves, have a look at our previous example converted to the Template Haskell version:

  1. {-# LANGUAGE OverloadedStrings #-}
  2. {-# LANGUAGE QuasiQuotes #-}
  3. {-# LANGUAGE TemplateHaskell #-}
  4. {-# LANGUAGE TypeFamilies #-}
  5. {-# LANGUAGE ViewPatterns #-}
  6. import Yesod.Core (RenderRoute (..), Yesod, mkYesod, parseRoutes,
  7. redirect, warp)
  8. -- | Our foundation datatype.
  9. data App = App
  10. mkYesod "App" [parseRoutes|
  11. / HomeR GET
  12. /fib/#Int FibR GET
  13. |]
  14. instance Yesod App
  15. getHomeR :: Handler ()
  16. getHomeR = redirect (FibR 1)
  17. fibs :: [Int]
  18. fibs = 0 : scanl (+) 1 fibs
  19. getFibR :: Int -> Handler String
  20. getFibR i = return $ show $ fibs !! i
  21. main :: IO ()
  22. main = warp 3000 App

What’s wonderful about this is, as the developer, you can now focus on the important part of your application, and not get involved in the details of writing parsers and renderers. There are of course some downsides to the usage of Template Haskell:

  • Compile times are a bit slower.

  • The details of what’s going on behind the scenes aren’t easily apparent. (Though you can use cabal haddock to see what identifiers have been generated for you.)

  • You don’t have as much fine-grained control. For example, in the Yesod route syntax, each dynamic parameter has to be a separate field in the route constructor, as opposed to bundling fields together. This is a conscious trade-off in Yesod between flexibility and complexity.

This usage of Template Haskell is likely the most controversial decision in Yesod. I personally think the benefits definitely justify its usage. But if you’d rather avoid Template Haskell, you’re free to do so. Every example so far in this chapter has done so, and you can follow those techniques. We also have another, simpler approach in the Yesod world: LiteApp.

LiteApp

LiteApp allows you to throw away type safe URLs and Template Haskell. It uses a simple routing DSL in pure Haskell. Once again, as a simple comparison, let’s rewrite our Fibonacci example to use it.

  1. import Data.Text (pack)
  2. import Yesod.Core (LiteHandler, dispatchTo, dispatchTo, liteApp,
  3. onStatic, redirect, warp, withDynamic)
  4. getHomeR :: LiteHandler ()
  5. getHomeR = redirect "/fib/1"
  6. fibs :: [Int]
  7. fibs = 0 : scanl (+) 1 fibs
  8. getFibR :: Int -> LiteHandler String
  9. getFibR i = return $ show $ fibs !! i
  10. main :: IO ()
  11. main = warp 3000 $ liteApp $ do
  12. dispatchTo getHomeR
  13. onStatic (pack "fib") $ withDynamic $ \i -> dispatchTo (getFibR i)

There you go, a simple Yesod app without any language extensions at all! However, even this application still demonstrates some type safety. Yesod will use fromPathPiece to convert the parameter for getFibR from Text to an Int, so any invalid parameter will be got by Yesod itself. It’s just one less piece of checking that you have to perform.

Shakespeare

While generating plain text pages can be fun, it’s hardly what one normally expects from a web framework. As you’d hope, Yesod comes built in with support for generating HTML, CSS and Javascript as well.

Before we get into templating languages, let’s do it the raw, low-level way, and then build up to something a bit more pleasant.

  1. import Data.Text (pack)
  2. import Yesod.Core
  3. getHomeR :: LiteHandler TypedContent
  4. getHomeR = return $ TypedContent typeHtml $ toContent
  5. "<html><head><title>Hi There!</title>\
  6. \<link rel='stylesheet' href='/style.css'>\
  7. \<script src='/script.js'></script></head>\
  8. \<body><h1>Hello World!</h1></body></html>"
  9. getStyleR :: LiteHandler TypedContent
  10. getStyleR = return $ TypedContent typeCss $ toContent
  11. "h1 { color: red }"
  12. getScriptR :: LiteHandler TypedContent
  13. getScriptR = return $ TypedContent typeJavascript $ toContent
  14. "alert('Yay, Javascript works too!');"
  15. main :: IO ()
  16. main = warp 3000 $ liteApp $ do
  17. dispatchTo getHomeR
  18. onStatic (pack "style.css") $ dispatchTo getStyleR
  19. onStatic (pack "script.js") $ dispatchTo getScriptR

We’re just reusing all of the TypedContent stuff we’ve already learnt. We now have three separate routes, providing HTML, CSS and Javascript. We write our content as Strings, convert them to Content using toContent, then wrap them with a TypedContent constructor to give them the appropriate content-type headers.

But as usual, we can do better. Dealing with Strings is not very efficient, and it’s tedious to have to manually put in the content type all the time. But we already know the solution to those problems: use the Html datatype from blaze-html. Let’s convert our getHomeR function to use it:

  1. import Data.Text (pack)
  2. import Text.Blaze.Html5 (toValue, (!))
  3. import qualified Text.Blaze.Html5 as H
  4. import qualified Text.Blaze.Html5.Attributes as A
  5. import Yesod.Core
  6. getHomeR :: LiteHandler Html
  7. getHomeR = return $ H.docTypeHtml $ do
  8. H.head $ do
  9. H.title $ toHtml "Hi There!"
  10. H.link ! A.rel (toValue "stylesheet") ! A.href (toValue "/style.css")
  11. H.script ! A.src (toValue "/script.js") $ return ()
  12. H.body $ do
  13. H.h1 $ toHtml "Hello World!"
  14. getStyleR :: LiteHandler TypedContent
  15. getStyleR = return $ TypedContent typeCss $ toContent
  16. "h1 { color: red }"
  17. getScriptR :: LiteHandler TypedContent
  18. getScriptR = return $ TypedContent typeJavascript $ toContent
  19. "alert('Yay, Javascript works too!');"
  20. main :: IO ()
  21. main = warp 3000 $ liteApp $ do
  22. dispatchTo getHomeR
  23. onStatic (pack "style.css") $ dispatchTo getStyleR
  24. onStatic (pack "script.js") $ dispatchTo getScriptR

Ahh, far nicer. blaze-html provides a convenient combinator library, and will execute far faster in most cases than whatever String concatenation you might attempt.

If you’re happy with blaze-html combinators, by all means use them. However, many people like to use a more specialized templating language. Yesod’s standard provider for this is the Shakespearean languages: Hamlet, Lucius, and Julius. You are by all means welcome to use a different system if so desired, the only requirement is that you can get a Content value from the template.

Since Shakespearean templates are compile-time checked, their usage requires either quasiquotation or Template Haskell. We’ll go for the former approach here. Please see the Shakespeare chapter in the book for more information.

  1. {-# LANGUAGE QuasiQuotes #-}
  2. import Data.Text (Text, pack)
  3. import Text.Julius (Javascript)
  4. import Text.Lucius (Css)
  5. import Yesod.Core
  6. getHomeR :: LiteHandler Html
  7. getHomeR = withUrlRenderer $
  8. [hamlet|
  9. $doctype 5
  10. <html>
  11. <head>
  12. <title>Hi There!
  13. <link rel=stylesheet href=/style.css>
  14. <script src=/script.js>
  15. <body>
  16. <h1>Hello World!
  17. |]
  18. getStyleR :: LiteHandler Css
  19. getStyleR = withUrlRenderer [lucius|h1 { color: red }|]
  20. getScriptR :: LiteHandler Javascript
  21. getScriptR = withUrlRenderer [julius|alert('Yay, Javascript works too!');|]
  22. main :: IO ()
  23. main = warp 3000 $ liteApp $ do
  24. dispatchTo getHomeR
  25. onStatic (pack "style.css") $ dispatchTo getStyleR
  26. onStatic (pack "script.js") $ dispatchTo getScriptR

URL rendering function

Likely the most confusing part of this is the withUrlRenderer calls. This gets into one of the most powerful features of Yesod: type-safe URLs. If you notice in our HTML, we’re providing links to the CSS and Javascript URLs via strings. This leads to a duplication of that information, as in our main function we have to provide those strings a second time. This is very fragile: our codebase is one refactor away from having broken links.

The recommended approach instead would be to use our type-safe URL datatype in our template instead of including explicit strings. As mentioned above, LiteApp doesn’t provide any meaningful type-safe URLs, so we don’t have that option here. But if you use the Template Haskell generators, you get type-safe URLs for free.

In any event, the Shakespearean templates all expect to receive a function to handle the rendering of a type-safe URL. Since we don’t actually use any type-safe URLs, just about any function would work here (the function will be ignored entirely), but withUrlRenderer is a convenient way of doing this.

As we’ll see next, withUrlRenderer isn’t really needed most of the time, since Widgets end up providing the renderer function for us automatically.

Widgets

Dealing with HTML, CSS and Javascript as individual components can be nice in many cases. However, when you want to build up reusable components for a page, it can get in the way of composability. If you want more motivation for why widgets are useful, please see the widget chapter. For now, let’s just dig into using them.

  1. {-# LANGUAGE QuasiQuotes #-}
  2. import Yesod.Core
  3. getHomeR :: LiteHandler Html
  4. getHomeR = defaultLayout $ do
  5. setTitle $ toHtml "Hi There!"
  6. [whamlet|<h1>Hello World!|]
  7. toWidget [lucius|h1 { color: red }|]
  8. toWidget [julius|alert('Yay, Javascript works too!');|]
  9. main :: IO ()
  10. main = warp 3000 $ liteApp $ dispatchTo getHomeR

This is the same example as above, but we’ve now condensed it into a single handler. Yesod will automatically handle providing the CSS and Javascript to the HTML. By default, it will place them in style and script tags in the head and body of the page, respectively, but Yesod provides many customization settings to do other things (such as automatically creating temporary static files and linking to them).

Widgets also have another advantage. The defaultLayout function is a member of the Yesod typeclass, and can be modified to provide a customized look-and-feel for your website. Many built-in pieces of Yesod, such as error messages, take advantage of the widget system, so by using widgets, you get a consistent feel throughout your site.

Details we won’t cover

Hopefully this chapter has pulled back enough of the “magic” in Yesod to let you understand what’s going on under the surface. We could of course continue using this approach for analyze the rest of the Yesod ecosystem, but that would be mostly redundant with the rest of this book. Hopefully you can now feel more informed as you read chapters on Persistent, forms, sessions, and subsites.