Back to Table of Contents

Web Routes

The web-routes libraries provide a system for type-safe url routing. The basic concept behind type-safe urls is very simple. Instead of working directly with url strings, we create a type that represents all the possible urls in our web application. By using types instead of strings we benefit in several ways:

fewer runtime errors due to typos
If you mistype the String, "/hmoe" instead of "/home", the compiler will gleefully compile it. But if you mistype the constructor as Hmoe instead of Home you will get a compile time error.
Compile type assurance that all routes are mapped to handlers
Routing is performed via a simple case statement on the url type. If you forget to handle a route, the compiler will give you an Pattern match(es) are non-exhaustive warning.
unique URLs for 3rd party libraries
Libraries (such as a blog or image gallery component) need a safe way to create urls that do no overlap with the routes provided by other libraries. For example, if a blog component and image component both try to claim the url /upload, something bad is going to happen. With web-routes, libraries do not have to take any special steps to ensure that the urls they generate are unique. web-routes are composable and result in unique urls.
Compile time errors when routes change
As a website evolves, existing routes might change or be removed entirely. With web-routes this will result in a change to the type. As a result, code that has not been updated will generate a compile-time error, instead of a runtime error. This is especially valuable when using 3rd party libraries, since you may not even be aware that the route had changed otherwise.
better separation of presentation and behavior
In web-routes, the parsing and printing of a url is separated from the mapping of a url to a handler or creating hyperlinks in your code. This makes it trivial to change the way the url type is converted to a string and back. You need only modify the function that does the conversion, and everything else can stay the same. You do not need to hunt all over the code trying to find places that use the old format.
self-documentation sitemap
Because the url type represents all the valid routes on your site, it also acts as a simple sitemap.

web-routes is designed to be very flexible. For example, it does not require that you use any particular mechanism for defining the mapping between the url type and the url string. Instead, we provide a variety of addon packages that provide different methods including, template-haskell, generics, parsec, quasi-quotation, and more. This means it is also easy to add your own custom mechanism. For example, you might still use template-haskell, but with a different set of rules for converting a type to a string.

web-routes is also not limited to use with any particular framework, templating system, database, etc. In fact, web-routes provides the foundation for type-safe url routing in yesod.

Web Routes Demo

Let's start by looking at a simple example of using web-routes. In this example we will use blaze for the html templates.

In order to run this demo you will need to install web-routes, web-routes-th and web-routes-happstack from hackage.

> {-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, TemplateHaskell #-}
> module Main where
>
> import Prelude                 hiding (head)
>
> import Control.Monad           (msum)
> import Data.Data               (Data, Typeable)
> import Data.Monoid             (mconcat)
> import Data.Text               (pack)
> import Happstack.Server        ( Response, ServerPartT, ok, toResponse, simpleHTTP
>                                , nullConf, seeOther, dir, notFound, seeOther)
> import Text.Blaze.Html4.Strict ( (!), html, head, body, title, p, toHtml
>                                , toValue, ol, li, a)
> import Text.Blaze.Html4.Strict.Attributes (href)
> import Web.Routes              ( PathInfo(..), RouteT, showURL
>                                , runRouteT, Site(..), setDefault, mkSitePI)
> import Web.Routes.TH           (derivePathInfo)
> import Web.Routes.Happstack    (implSite)
>

First we need to define the type to represent our routes. In this site we will have a homepage and articles which can be retrieved by their id.

> newtype ArticleId
>     = ArticleId { unArticleId :: Int }
>       deriving (Eq, Ord, Enum, Read, Show, Data, Typeable, PathInfo)
>
> data Sitemap
>     = Home
>     | Article ArticleId
>       deriving (Eq, Ord, Read, Show, Data, Typeable)
>

Next we use template-haskell to derive an instance of PathInfo for the Sitemap type.

> $(derivePathInfo ''Sitemap)
>

The PathInfo class is defined in Web.Routes and looks like this:

> class PathInfo a where
>   toPathSegments :: a -> [String]
>   fromPathSegments :: URLParser a

It is basically a class that describes how to turn a type into a url and back. This class is semi-optional. Some conversion methods such as web-routes-th and web-routes-regular use it, but others do not.

Since ArticleId is just a newtype we were able to just do deriving PathInfo instead of having to call derivePathInfo.

Next we need a function that maps a route to the handlers:

> route :: Sitemap -> RouteT Sitemap (ServerPartT IO) Response
> route url =
>     case url of
>       Home                -> homePage
>       (Article articleId) -> articlePage articleId
>

As you can see, mapping a url to a handler is just a straight-forward case statement. We do not need to do anything fancy here to extract the article id from the url, becuse that has already been done when the url was converted to a Sitemap value.

You may be tempted to write the route function like this instead of using the case statement:

> route :: Sitemap -> RouteT Sitemap (ServerPartT IO) Response
> route Home                = homePage
> route (Article articleId) = articlePage articleId

But, I don't recommend it. In a real application, the route function will likely take a number of extra arguments such as database handles. Sometimes those extra arguments are only used by some of the handlers. But every time you add a parameter, you have to update every pattern match to account for the extra argument. Using a case statement instead makes the code easier to maintain and more readable in my opinion.

The other thing you will notice is the RouteT monad transformer in the type signature. The RouteT monad transformer is another semi-optional feature of web-routes. RouteT is basically a Reader monad that holds the function which converts the url type into a string. At first, this seems unnecessary -- why not just call toPathInfo directly and skip RouteT entirely? But it turns out there are few advantages that RouteT brings:

  1. RouteT is parametrized by the url type -- in this case Sitemap. That will prevent us from accidentally trying to convert an ArticleId into a url. An ArticleId is a valid component of some urls, but it is not a valid URL by itself.
  2. The url showing function inside RouteT can also contain additional information needed to form a valid url, such as the hostname name, port, and path prefix
  3. RouteT is also used when we want to embed a library/sub-site into a larger site.

We will see examples of these benefits as we continue with the tutorial.

Next, we have the handler functions:

> homePage :: RouteT Sitemap (ServerPartT IO) Response
> homePage =
>     do articles <- mapM mkArticle [(ArticleId 1) .. (ArticleId 10)]
>        ok $ toResponse $
>           html $ do
>             head $ title $ (toHtml "Welcome Home!")
>             body $ do
>               ol $ mconcat articles
>     where
>       mkArticle articleId =
>           do url <- showURL (Article articleId)
>              return $ li $ a ! href (toValue url) $
>                         toHtml $ "Article " ++ (show $ unArticleId articleId)
>
> articlePage :: ArticleId -> RouteT Sitemap (ServerPartT IO) Response
> articlePage (ArticleId articleId) =
>     do homeURL <- showURL Home
>        ok $ toResponse $
>           html $ do
>             head $ title $ (toHtml $ "Article " ++ show articleId)
>             body $ do
>                    p $ toHtml $ "You are now reading article " ++ show articleId
>                    p $ do toHtml "Click "
>                           a ! href (toValue homeURL) $ toHtml "here"
>                           toHtml " to return home."
>

Even though we have the RouteT in the type signature -- these functions look normal ServerPartT functions -- we do not have to use lift or anything else. That is because RouteT is a instance of all the Happstack classes such as ServerMonad, FilterMonad, etc. Though you do need to make sure you have imported Web.Routes.Happstack to get those instances.

The only new thing here is the showURL function, which has the type:

> showURL :: ShowURL m => URL m -> m String

showURL converts a url type, such as Sitemap into a url String that we can use an href, src, etc attribute.

URL m is a type-function that calculates the url type based on the monad we are currently in. For RouteT url m a, URL m is going to be whatever url is. In this example, url is Sitemap. If you are not familiar with type families and type functions, see this section.

Now we have:

  1. A url type, Sitemap
  2. functions to convert the type to a string and back via PathInfo
  3. a function to route the url to a handler, route

We need to tie these three pieces together. That is what the Site type does for us:

> data Site url a
>     = Site {
>            -- | function which routes the url to a handler
>              handleSite         :: (url -> [(String, String)] -> String) -> url -> a
>            -- | This function must be the inverse of 'parsePathSegments'.
>            , formatPathSegments :: url -> ([String], [(String, String)])
>            -- | This function must be the inverse of 'formatPathSegments'.
>            , parsePathSegments  :: [String] -> Either String url
>            }

Looking at the type for Site, we notice that it is very general -- it does not have any references to Happstack, PathInfo, URLParser, RouteT, etc. That is because those are all addons to the core of web-routes. We can convert our route to a Site using some simple helper functions like this:

> site :: Site Sitemap (ServerPartT IO Response)
> site =
>        setDefault Home $ mkSitePI (runRouteT route)
>

runRouteT removes the RouteT wrapper from our routing function:

> runRouteT :: (url -> RouteT url m a)
>           -> ((url -> [(String, String)] -> String) -> url -> m a)

So if we have our routing function like:

> route :: Sitemap
>       -> RouteT Sitemap (ServerPartT IO) Response

runRouteT will convert it to a function that takes a url showing function:

> (runRouteT route) :: (Sitemap -> [(String, String)] -> String)
>                   -> Sitemap
>                   -> ServerPartT IO Response

Since we created a PathInfo instance for Sitemap we can use mkSitePI to convert the new function to a Site. mkSitePI has the type:

> mkSitePI :: (PathInfo url) =>
>             ((url -> [(String, String)] -> String) -> url -> a)
>          -> Site url a

so applying it to runRouteT route gives us:

> (mkSitePI (runRouteT route)) :: Site Sitemap (ServerPartT IO Response)

setDefault allows you to map / to any route you want. In this example we map / to Home.

> setDefault :: url -> Site url a -> Site url a

Next we use implSite to embed the Site into a normal Happstack route:

> main :: IO ()
> main = simpleHTTP nullConf $
>        msum [ dir "favicon.ico" $ notFound (toResponse ())
>             , implSite (pack "http://localhost:8000") (pack "/route") site
>             , seeOther "/route" (toResponse ())
>             ]
>

The type for implSite is straight-forward:

> implSite :: (Functor m, Monad m, MonadPlus m, ServerMonad m) =>
>             String         -- ^ "http://example.org"
>          -> FilePath       -- ^ path to this handler, .e.g. "/route"
>          -> Site url (m a) -- ^ the 'Site'
>          -> m a

The first argument is the domain/port/etc that you want to add to the beginning of any URLs you show. The first argument is not used during the decoding/routing process -- it is merely prepended to any generated url strings.

The second argument is the path to this handler. This path is automatically used when routing the incoming request and when showing the URL. This path can be used to ensure that all routes generated by web-routes are unique because they will be in a separate sub-directory (aka, a separate namespace). If you do not want to put the routes in a separate sub-directory you can set this field to "".

The third argument is the Site that does the routing.

If the URL decoding fails, then implSite will call mzero.

Sometimes you will want to know the exact parse error that caused the router to fail. You can get the error by using implSite_ instead. Here is an alternative main that prints the route error to stdout.

> main :: IO ()
> main = simpleHTTP nullConf $
>        msum [ dir "favicon.ico" $ notFound (toResponse ())
>             , do r <- implSite_ (pack "http://localhost:8000") (pack "/route") site
>                  case r of
>                    (Left e) -> liftIO (print e) >> mzero
>                    (Right m) -> return m
>             , seeOther "/route" (toResponse ())
>             ]
>

[Source code for the app is here.]

Web Routes + Type Families

showURL has the type:

> showURL :: ShowURL m => URL m -> m String

If you are not familiar with type families and type functions, the URL m in that type signature might look a bit funny. But it is really very simple.

The showURL function leverages the ShowURL class:

> class ShowURL m where
>    type URL m
>    showURLParams :: (URL m) -> [(String, String)] -> m String

And here is the RouteT instance for ShowURL:

> instance (Monad m) => ShowURL (RouteT url m) where
>    type URL (RouteT url m) = url
>    showURLParams url params =
>        do showF <- askRouteT
>           return (showF url params)

Here URL is a type function that is applied to a type and gives us another type. For example, writing URL (RouteT Sitemap (ServerPartT IO)) gives us the type Sitemap. We can use the type function any place we would normally use a type.

In our example we had:

> homeURL <- showURL Home

So there, showURL is going to have the type:

> showURL :: URL (RouteT Sitemap (ServerPartT IO))
>         -> RouteT Sitemap (ServerPartT IO) String

which can be simplified to:

> showURL :: Sitemap -> RouteT Sitemap (ServerPartT IO) String

So, we see that the url type we pass to showURL is dictated by the monad we are currently in. This ensures that we only call showURL on values of the right type.

While ShowURL is generally used with the RouteT type -- it is not actually a requirement. You can implement ShowURL for any monad of your choosing.

Web Routes Boomerang

In the previous example we used template haskell to automatically derive a mapping between the url type and the url string. This is very convenient early in the development process when the routes are changing a lot. But the resulting urls are not very attractive. One solution is to write the mappings from the url type to the url string by hand.

One way to do that would be to write one function to show the urls, and another function that uses parsec to parse the urls. But having to say the same thing twice is really annoying and error prone. What we really want is a way to write the mapping once, and automatically exact a parser and printer from the specification.

Fortunately, Sjoerd Visscher and Martijn van Steenbergen figured out exactly how to do that and published a proof of concept library know as Zwaluw. With permission, I have refactored their original library into two separate libraries: boomerang and web-routes-boomerang.

The technique behind Zwaluw and Boomerang is very cool. But in this tutorial we will skip the theory and get right to the practice.

In order to run this demo you will need to install web-routes, web-routes-boomerang and web-routes-happstack from hackage.

We will modify the previous demo to use boomerang in order to demonstrate how easy it is to change methods midstream. We will also add a few new routes to demonstrate some features of using boomerang.

> {-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, TemplateHaskell, 
>   TypeOperators, OverloadedStrings #-}
> module Main where
>

The first thing to notice is that we hide id and (.) from the Prelude and import the versions from Control.Category instead.

> import Prelude                 hiding (head, id, (.))
> import Control.Category        (Category(id, (.)))
>
> import Control.Monad           (msum)
> import Data.Data               (Data, Typeable)
> import Data.Monoid             (mconcat)
> import Data.String             (fromString)
> import Data.Text               (Text)
> import Happstack.Server        ( Response, ServerPartT, ok, toResponse, simpleHTTP
>                                , nullConf, seeOther, dir, notFound, seeOther)
> import Text.Blaze.Html4.Strict ( (!), html, head, body, title, p, toHtml
>                                , toValue, ol, li, a)
> import Text.Blaze.Html4.Strict.Attributes (href)
> import Text.Boomerang.TH       (derivePrinterParsers)
> import Web.Routes              ( PathInfo(..), RouteT, showURL
>                                , runRouteT, Site(..), setDefault, mkSitePI)
> import Web.Routes.TH           (derivePathInfo)
> import Web.Routes.Happstack    (implSite)
> import Web.Routes.Boomerang
>

Next we have our Sitemap types again. Sitemap is similar to the previous example, except it also includes UserOverview and UserDetail.

> newtype ArticleId
>     = ArticleId { unArticleId :: Int }
>       deriving (Eq, Ord, Enum, Read, Show, Data, Typeable, PathInfo)
>
> data Sitemap
>     = Home
>     | Article ArticleId
>     | UserOverview
>     | UserDetail Int Text
>       deriving (Eq, Ord, Read, Show, Data, Typeable)
>

Next we call derivePrinterParsers:

> $(derivePrinterParsers ''Sitemap)
>

That will create new combinators corresponding to the constructors for Sitemap. They will be named, rHome, rArticle, rUserOverview, and rUserDetail.

Now we can specify how the Sitemap type is mapped to a url string and back:

> sitemap :: Router () (Sitemap :- ())
> sitemap =
>     (  rHome
>     <> rArticle . (lit "article" </> articleId)
>     <> lit "users" . users
>     )
>     where
>       users =  rUserOverview
>             <> rUserDetail </> int . lit "-" . anyText
>
> articleId :: Router () (ArticleId :- ())
> articleId =
>     xmaph ArticleId (Just . unArticleId) int

The mapping looks like this:

urltype
/<=>Home
/article/int<=>Article int
/users<=>UserOverview
/users/int-string<=>UserDetail int string

The sitemap function looks like an ordinary parser. But, what makes it is exciting is that it also defines the pretty-printer at the same time.

By examining the mapping table and comparing it to the code, you should be able to get an intuitive feel for how boomerang works. The key boomerang features we see are:

<>
<> is the choice operator. It chooses between the various paths.
.
. is used to combine elements together.
</>
the combinators, such as lit, int, anyText, operate on a single path segment. </> matches on the / between path segments.
lit
lit matches on a string literal. If you enabled OverloadedStrings then you do not need to explicitly use the lit function. For example, you could just write, int . "-" . anyText.
int
int matches on an Int.
anyText
anyText matches on any string. It keeps going until it reaches the end of the current path segment.
xmaph
xmaph is a bit like fmap, except instead of only needing a -> b it also needs the other direction, b -> Maybe a.
> xmaph :: (a -> b)
>       -> (b -> Maybe a)
>       -> PrinterParser e tok i (a :- o)
>       -> PrinterParser e tok i (b :- o)
In this example, we use xmaph to convert int :: Router () (Int :- ()) into articleId :: Router () (ArticleId :- ()).
longest route
You will notice that the parser for /users comes before /users/int-string. Unlike parsec, the order of the parsers (usually) does not matter. We also do not have to use try to allow for backtracking. boomerang will find all valid parses and pick the best one. Here, that means the parser that consumed all the available input.

Router type is just a simple alias:

> type Router a b = PrinterParser TextsError [Text] a b

Looking at this line:

>             <> rUserDetail </> int . lit "-" . anyText

and comparing it to the constructor

>     UserDetail Int Text

we see that the constructor takes two arguments, but the mapping uses three combinators, int, lit, and anyText. It turns out that some combinators produce/consume values from the url type, and some do not. We can find out which do and which don't by looking at the their types:

> int     ::         PrinterParser TextsError [Text] r (Int :- r)
> anyText ::         PrinterParser TextsError [Text] r (Text :- r)
> lit     :: Text -> PrinterParser TextsError [Text] r r

We see int takes r and produces (Int :- r) and anyText takes r and produces (Text :- r). While lit takes r and returns r.

Looking at the type of the all three composed together we get:

> int . lit "-" . anyText :: PrinterParser TextsError [Text] a (Int :- (Text :- a))

So there we see the Int and Text that are arguments to UserDetail.

Looking at the type of rUserDetail, we will see that it has the type:

>  rUserDetail :: PrinterParser e tok (Int :- (Text :- r)) (Sitemap :- r)

So, it takes an Int and Text and produces a Sitemap. That mirrors what the UserDetail constructor itself does:

ghci> :t UserDetail
UserDetail :: Int -> Text -> Sitemap

Next we need a function that maps a route to the handlers. This is the same exact function we used in the previous example extended with the additional routes:

> route :: Sitemap -> RouteT Sitemap (ServerPartT IO) Response
> route url =
>     case url of
>       Home                  -> homePage
>       (Article articleId)   -> articlePage articleId
>       UserOverview          -> userOverviewPage
>       (UserDetail uid name) -> userDetailPage uid name
>

Next, we have the handler functions. These are also exactly the same as the previous example, plus the new routes:

> homePage :: RouteT Sitemap (ServerPartT IO) Response
> homePage =
>     do articles     <- mapM mkArticle [(ArticleId 1) .. (ArticleId 10)]
>        userOverview <- showURL UserOverview
>        ok $ toResponse $
>           html $ do
>             head $ title $ "Welcome Home!"
>             body $ do
>               a ! href (toValue userOverview) $ "User Overview"
>               ol $ mconcat articles
>     where
>       mkArticle articleId =
>           do url <- showURL (Article articleId)
>              return $ li $ a ! href (toValue url) $
>                         toHtml $ "Article " ++ (show $ unArticleId articleId)
>
> articlePage :: ArticleId -> RouteT Sitemap (ServerPartT IO) Response
> articlePage (ArticleId articleId) =
>     do homeURL <- showURL Home
>        ok $ toResponse $
>           html $ do
>             head $ title $ (toHtml $ "Article " ++ show articleId)
>             body $ do
>                    p $ toHtml $ "You are now reading article " ++ show articleId
>                    p $ do "Click "
>                           a ! href (toValue homeURL) $ "here"
>                           " to return home."
>
> userOverviewPage :: RouteT Sitemap (ServerPartT IO) Response
> userOverviewPage =
>     do users <- mapM mkUser [1 .. 10]
>        ok $ toResponse $
>           html $ do
>             head $ title $ "Our Users"
>             body $ do
>               ol $ mconcat users
>     where
>       mkUser userId =
>           do url <- showURL (UserDetail userId (fromString $ "user " ++ show userId))
>              return $ li $ a ! href (toValue url) $
>                         toHtml $ "User " ++ (show $ userId)
>
> userDetailPage :: Int -> Text -> RouteT Sitemap (ServerPartT IO) Response
> userDetailPage userId userName =
>     do homeURL <- showURL Home
>        ok $ toResponse $
>           html $ do
>             head $ title $ (toHtml $ "User " <> userName)
>             body $ do
>                    p $ toHtml $ "You are now view user detail page for " <> userName
>                    p $ do "Click "
>                           a ! href (toValue homeURL) $ "here"
>                           " to return home."
>

Creating the Site type is similar to the previous example. We still use runRouteT to unwrap the RouteT layer. But now we use boomerangSite to convert the route function into a Site:

> site :: Site Sitemap (ServerPartT IO Response)
> site =
>        setDefault Home $ boomerangSite (runRouteT route) sitemap
>

The route function is essentially the same in this example and the previous example -- it did not have to be changed to work with boomerang instead of PathInfo. It is the formatPathSegments and parsePathSegments functions bundled up in the Site that change. In the previous example, we used mkSitePI, which leveraged the PathInfo instances. Here we use boomerangSite which uses the sitemap mapping we defined above.

The practical result is that you can start by using derivePathInfo and avoid having to think about how the urls will look. Later, once the routes have settled down, you can then easily switch to using boomerang to create your route mapping.

Next we use implSite to embed the Site into a normal Happstack route:

> main :: IO ()
> main = simpleHTTP nullConf $
>        msum [ dir "favicon.ico" $ notFound (toResponse ())
>             , implSite "http://localhost:8000" "/route" site
>             , seeOther ("/route/" :: String) (toResponse ())
>             ]
>

[Source code for the app is here.]

In this example, we only used a few simple combinators. But boomerang provides a whole range of combinators such as many, some, chain, etc. For more information check out the haddock documentation for boomerang. Especially the Text.Boomerang.Combinators and Text.Boomerang.Texts modules.

web-routes and HSP

You will need to install the optional web-routes, web-routes-th, web-routes-hsp and happstack-hsp packages for this section.

> {-# LANGUAGE TemplateHaskell #-}
> {-# OPTIONS_GHC -F -pgmFtrhsx #-}
> module Main where
>
> import Control.Applicative ((<$>))
> import Control.Monad       (msum)
> import Data.Text           (empty, pack)
> import Happstack.Server
> import Happstack.Server.HSP.HTML
> import Web.Routes
> import Web.Routes.TH
> import Web.Routes.XMLGenT
> import Web.Routes.Happstack

If you are using web-routes and HSP then inserting URLs is especially clean and easy. If we have the URL:

> data SiteURL = Monkeys Int deriving (Eq, Ord, Read, Show)
>
> $(derivePathInfo ''SiteURL)
>

Now we can define a template like this:

> monkeys :: Int -> RouteT SiteURL (ServerPartT IO) Response
> monkeys n =
>     do html <- defaultTemplate "monkeys" () $
>         <%>
>          <p>You have <% show n %> monkeys.</p>
>          <p>Click <a href=(Monkeys (succ n))>here</a> for more.</p>
>         </%>
>        ok $ (toResponse html)

Notice that in particular this bit:

> <a href=(Monkeys (succ n))>here</a>

We do not need showURL, we just use the URL type directly. That works because Web.Routes.XMLGenT provides an instance:

> instance (Functor m, Monad m) => EmbedAsAttr (RouteT url m) (Attr String url)

Here is the rest of the example:

> route :: SiteURL -> RouteT SiteURL (ServerPartT IO) Response
> route url =
>     case url of
>       (Monkeys n) -> monkeys n
>
> site :: Site SiteURL (ServerPartT IO Response)
> site = setDefault (Monkeys 0) $ mkSitePI (runRouteT route)
>
> main :: IO ()
> main = simpleHTTP nullConf $
>   msum [ dir "favicon.ico" $ notFound (toResponse ())
>        , implSite (pack "http://localhost:8000") empty site
>        ]

[Source code for the app is here.]

Still to come

I am working on additional sections which will cover creating 'sub-sites' that can be embedded into larger sites, integration with HSP, and more.

Next: Acid State