If we look at Yesod as a model-view-controller framework, routing and handlers make up the controller. For contrast, let’s describe two other routing approaches used in other web development environments:
Dispatch based on filename. This is how PHP and ASP work, for example.
Have a centralized routing function that parses routes based on regular expressions. Django and Rails follow this approach.
Yesod is closer in principle to the latter technique. Even so, there are significant differences. Instead of using regular expressions, Yesod matches on pieces of a route. Instead of having a one-way route-to-handler mapping, Yesod has an intermediate data type (called the route or type-safe URL data type) and creates two-way conversion functions.
Coding this more advanced system manually is tedious and error prone. Therefore, Yesod defines a domain-specific language (DSL) for specifying routes, and provides Template Haskell functions to convert this DSL to Haskell code. This chapter will explain the syntax of the routing declarations, give you a glimpse of what code is generated for you, and explain the interaction between routing and handler functions.
Instead of trying to shoehorn route declarations into an existing syntax, Yesod’s approach is to use a simplified syntax designed just for routes. This has the advantage of making the code not only easy to write, but simple enough that someone with no Yesod experience can read and understand the sitemap of your application.
A basic example of this syntax is:
/ HomeR GET /blog BlogR GET POST /blog/#BlogId BlogPostR GET POST /static StaticR Static getStatic
The next few sections explain the full details of what goes on in the route declaration.
One of the first things Yesod does when it gets a request is split up the requested path into pieces. The pieces are tokenized at all forward slashes. For example:
toPieces
"/"
=
[]
toPieces
"/foo/bar/baz/"
=
[
"foo"
,
"bar"
,
"baz"
,
""
]
You may notice that there are some funny things going on with trailing slashes, or double slashes (/foo//bar//), or a few other things. Yesod believes in having canonical URLs; if users request a URL with a trailing slash, or with a double slash, they are automatically redirected to the canonical version. This ensures you have one URL for one resource, and can help with your search rankings.
What this means for you is that you needn’t concern yourself with the exact structure of your URLs: you can safely think about pieces of a path, and Yesod automatically handles intercalating the slashes and escaping problematic characters.
If, by the way, you want more fine-tuned control of how paths are split into
pieces and joined together again, you’ll want to look at the cleanPath
and
joinPath
methods in Chapter 6.
When you are declaring your routes, you have three types of pieces at your disposal:
This is a plain string that must be matched against precisely in the URL.
This is a single piece (i.e., between two forward slashes), but
represents a user-submitted value. This is the primary method of receiving
extra user input on a page request. These pieces begin with a hash (#
) and are
followed by a data type. The data type must be an instance of PathPiece
.
The same as the previous type, but can receive multiple pieces of the URL.
This must always be the last piece in a resource pattern. It is specified by an
asterisk (*
) followed by a data type, which must be an instance of
PathMultiPiece
. Multipieces are not as common as the other two, though they
are very important for implementing features like static trees representing
file structure or wikis with arbitrary hierarchies.
Since Yesod 1.4, you can additionally use a +
to indicate a dynamic
multi. This is important, because the C preprocessor can be confused by the /*
character combination.
Let us take a look at some standard kinds of resource patterns you may want to write. Starting simply, the root of an application will just be /. Similarly, you may want to place your FAQ at /page/faq.
Now let’s say we are going to write a Fibonacci website. We might construct our URLs like /fib/#Int. But there’s a slight problem with this: we do not want to allow negative numbers or zero to be passed into our application. Fortunately, the type system can protect us:
newtype
Natural
=
Natural
Int
instance
PathPiece
Natural
where
toPathPiece
(
Natural
i
)
=
T
.
pack
$
show
i
fromPathPiece
s
=
case
reads
$
T
.
unpack
s
of
(
i
,
""
)
:
_
|
i
<
1
->
Nothing
|
otherwise
->
Just
$
Natural
i
[]
->
Nothing
On line 1 we define a simple newtype
wrapper around Int
to protect ourselves
from invalid input. We can see that PathPiece
is a typeclass with two
methods. toPathPiece
does nothing more than convert to a Text
.
fromPathPiece
attempts to convert a Text
to our data type, returning
Nothing
when this conversion is impossible. By using this data type, we can
ensure that our handler function is only ever given natural numbers, allowing
us to once again use the type system to battle the boundary issue.
In a real-life application, we would also want to ensure we never
accidentally constructed an invalid Natural
value internally to our app. To do
so, we could use an approach like smart constructors.
For the purposes of this example, we’ve kept the code simple.
Defining a PathMultiPiece
is just as simple. Let’s say we want to have a wiki with at least two levels of hierarchy. We might define a data type such as:
data
Page
=
Page
Text
Text
[
Text
]
-- 2 or more
instance
PathMultiPiece
Page
where
toPathMultiPiece
(
Page
x
y
z
)
=
x
:
y
:
z
fromPathMultiPiece
(
x
:
y
:
z
)
=
Just
$
Page
x
y
z
fromPathMultiPiece
_
=
Nothing
By default, Yesod will ensure that no two routes have the potential to overlap with each other. So, for example, consider the following routes:
/foo/bar Foo1R GET /foo/#Text Foo2R GET
This route declaration will be rejected as overlapping, because /foo/bar
will
match both routes. However, there are two cases where you may wish to allow
overlapping:
If you know by the definition of your data type that the overlap can never happen. For example, if you replace Text
with Int
in the preceding example, it’s easy to convince yourself that there’s no route that exists that will overlap. Yesod is currently not capable of performing such an analysis.
If you have some extra knowledge about how your application operates, and know that such a situation should never be allowed—for example, if the Foo2R
route should never be allowed to receive the parameter bar
.
You can turn off overlap checking by using an exclamation mark at the beginning of your route. For example, the following will be accepted by Yesod:
/foo/bar Foo1R GET !/foo/#Int Foo2R GET !/foo/#Text Foo3R GET
You can also place the exclamation point at the beginning of any of the
path pieces, or following the #
, *
, or +
characters. However, this newer
syntax should be preferred as it’s clearer what the goal is.
One issue that overlapping routes introduces is ambiguity. In the preceding example,
should /foo/bar
route to Foo1R
or Foo3R
? And should /foo/42
route to
Foo2R
or Foo3R
? Yesod’s rule for this is simple: the first route wins.
Each resource pattern also has a name associated with it. That name will become the constructor for the type-safe URL data type associated with your application. Therefore, it has to start with a capital letter. By convention, these resource names all end with a capital R. There is nothing forcing you to do this; it is just common practice.
The exact definition of our constructor depends on the resource pattern it is attached to. Whatever data types are used as single pieces or multipieces of the pattern become arguments to the data type. This gives us a one-to-one correspondence between our type-safe URL values and valid URLs in our application.
This doesn’t necessarily mean that every value is a working page, just
that it is a potentially valid URL. As an example, the value PersonR "Michael"
may not resolve to a valid page if there is no Michael in the
database.
Let’s get some real examples going here. If you had the resource patterns
/person/#Text
named PersonR
, /year/#Int
named YearR
, and /page/faq
named FaqR
, you would end up with a route data type roughly looking like:
data
MyRoute
=
PersonR
Text
|
YearR
Int
|
FaqR
If a user requests /year/2009
, Yesod will convert it into the value YearR 2009
. /person/Michael
becomes PersonR "Michael"
, and /page/faq
becomes
FaqR
. On the other hand, /year/two-thousand-nine
, /person/michael/snoyman
,
and /page/FAQ
would all result in 404 errors without ever seeing your code.
The last piece of the puzzle when declaring your resources is how they will be handled. There are three options in Yesod:
A single handler function for all request methods on a given route.
A separate handler function for each request method on a given route. Any other request method will generate a 405 Method Not Allowed response.
You want to pass off to a subsite.
The first two can be easily specified. A single handler function will be a line
with just a resource pattern and the resource name, such as /page/faq FaqR
.
In this case, the handler function must be named handleFaqR
.
A separate handler for each request method will be the same, plus a list of
request methods. The request methods must be in all capital letters; for example,
/person/#String PersonR GET POST DELETE
. In this case, you would need to
define three handler functions: getPersonR
, postPersonR
, and
deletePersonR
.
Subsites are a very useful—but more complicated—topic in Yesod. We will cover
writing subsites later, but using them is not too difficult. The most commonly
used subsite is the static subsite, which serves static files for your
application. In order to serve static files from /static
, you would need a
resource line like:
/static StaticR Static getStatic
In this line, /static
just says where in your URL structure to serve the
static files from. There is nothing magical about the word “static”; you could
easily replace it with /my/non-dynamic/files
.
The next word, StaticR
, gives the resource name. The next two words
specify that we are using a subsite. Static
is the name of the subsite
foundation data type, and getStatic
is a function that gets a Static
value from a value of your master foundation data type.
Let’s not get too caught up in the details of subsites now. We will look more closely at the static subsite in Chapter 15.
Once you have specified your routes, Yesod will take care of all the pesky
details of URL dispatch for you. You just need to make sure to provide the
appropriate handler functions. For subsite routes, you do not need to write any
handler functions, but you do for the other two. We mentioned the naming rules
earlier (MyHandlerR GET
becomes getMyHandlerR
, MyOtherHandlerR
becomes
handleMyOtherHandlerR
).
Now that we know which functions we need to write, let’s figure out what their type signatures should be.
Let’s look at a simple handler function:
mkYesod
"Simple"
[
parseRoutes
|
/
HomeR
GET
|
]
getHomeR
::
Handler
Html
getHomeR
=
defaultLayout
[
whamlet
|<
h1
>
This
is
simple
|
]
There are two components to this return type: Handler
and Html
. Let’s
analyze each in more depth.
Like the Widget
type, the Handler
data type is not defined anywhere in the
Yesod libraries. Instead, the libraries provide the data type:
data
HandlerT
site
m
a
And like WidgetT
, this has three arguments: a base monad m
, a monadic value
a
, and the foundation data type site
. Each application defines a Handler
synonym that constrains site
to that application’s foundation data type, and
sets m
to IO
. If your foundation is MyApp
, in other words, you’d have the
synonym:
type
Handler
=
HandlerT
MyApp
IO
We need to be able to modify the underlying monad when writing subsites, but
otherwise we’ll use IO
.
The HandlerT
monad provides access to information about the user request
(e.g., query string parameters), allows modifying the response (e.g., response
headers), and more. This is the monad that most of your Yesod code will live
in.
In addition, there’s a typeclass called MonadHandler
. Both HandlerT
and
WidgetT
are instances of this typeclass, allowing many common functions to
be used in both monads. If you see MonadHandler
in any API documentation, you
should remember that the function can be used in your Handler
functions.
There’s nothing too surprising about this type. This function returns some HTML
content, represented by the Html
data type. But clearly Yesod would not be
useful if it only allowed HTML responses to be generated. We want to respond with
CSS, JavaScript, JSON, images, and more. So the question is: what data types
can be returned?
In order to generate a response, we need to know two pieces of information:
the content type (e.g., text/html
, image/png
) and how to serialize it to a
stream of bytes. This is represented by the TypedContent
data type:
data
TypedContent
=
TypedContent
!
ContentType
!
Content
We also have a typeclass for all data types, which can be converted to a
TypedContent
:
class
ToTypedContent
a
where
toTypedContent
::
a
->
TypedContent
Many common data types are instances of this typeclass, including Html
,
Value
(from the aeson
package, representing JSON), Text
, and even ()
(for
representing an empty response).
Let’s return to our simple example:
mkYesod
"Simple"
[
parseRoutes
|
/
HomeR
GET
|
]
getHomeR
::
Handler
Html
getHomeR
=
defaultLayout
[
whamlet
|<
h1
>
This
is
simple
|
]
Not every route is as simple as this HomeR
. Take, for instance, our PersonR
route from earlier. The name of the person needs to be passed to the handler
function. This translation is very straightforward, and hopefully intuitive.
For example:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
import
Data.Text
(
Text
)
import
qualified
Data.Text
as
T
import
Yesod
data
App
=
App
instance
Yesod
App
mkYesod
"App"
[
parseRoutes
|
/
person
/#
Text
PersonR
GET
/
year
/#
Integer
/
month
/#
Text
/
day
/#
Int
DateR
/
wiki
/*
Texts
WikiR
GET
|
]
getPersonR
::
Text
->
Handler
Html
getPersonR
name
=
defaultLayout
[
whamlet
|<
h1
>
Hello
#
{
name
}
!|
]
handleDateR
::
Integer
->
Text
->
Int
->
Handler
Text
-- text/plain
handleDateR
year
month
day
=
return
$
T
.
concat
[
month
,
" "
,
T
.
pack
$
show
day
,
", "
,
T
.
pack
$
show
year
]
getWikiR
::
[
Text
]
->
Handler
Text
getWikiR
=
return
.
T
.
unwords
main
::
IO
()
main
=
warp
3000
App
The arguments have the types of the dynamic pieces for each route, in the order
specified. Also notice how we are able to use both Html
and Text
return
values.
Because the majority of your code will live in the Handler
monad, it’s
important to invest some time in understanding it better. The remainder of this
chapter will give a brief introduction to some of the most common functions
living in the Handler
monad. I am specifically not covering any of the
session functions; those will be addressed in Chapter 9.
There are a number of functions that return information about your application as a whole, and give no information about individual requests. Some of these are:
getYesod
Returns your application foundation value. If you store configuration
values in your foundation, you will probably end up using this function a lot.
(If you’re so inclined, you can also use ask
from Control.Monad.Reader
;
getYesod
is simply a type-constrained synonym for it.)
getUrlRender
Returns the URL rendering function, which converts a type-safe
URL into a Text
. Most of the time—like with Hamlet—Yesod calls this
function for you, but you may occasionally need to call it directly.
getUrlRenderParams
A variant of getUrlRender
that converts both a type-safe
URL and a list of query string parameters. This function handles all
percent-encoding necessary.
The most common information you will want to get about the current request is
the requested path, the query string parameters, and POST
ed form data. The
first of those is dealt with in the routing, as described earlier. The other two
are best dealt with using the forms module.
That said, you will sometimes need to get the data in a more raw format. For
this purpose, Yesod exposes the YesodRequest
data type along with the
getRequest
function to retrieve it. This gives you access to the full list of GET
parameters, cookies, and preferred languages. There are some convenient
functions to make these lookups easier, such as lookupGetParam
,
lookupCookie
, and languages
. For raw access to the POST
parameters, you
should use runRequestBody
.
If you need even more raw data, like request headers, you can use waiRequest
to access the Web Application Interface (WAI) request value. See Appendix B for more details.
The following functions immediately end execution of a handler function and return a result to the user:
redirect
Sends a redirect response to the user (a 303 response). If you want to use a different response code (e.g., a permanent 301 redirect), you can use redirectWith
.
Yesod uses a 303 response for HTTP/1.1 clients, and a 302 response for HTTP/1.0 clients. You can read up on this sordid saga in the HTTP spec.
notFound
Returns a 404 response. This can be useful if a user requests a database value that doesn’t exist.
permissionDenied
invalidArgs
sendFile
Sends a file from the filesystem with a specified content type. This
is the preferred way to send static files, because the underlying WAI handler may
be able to optimize this to a sendfile
system call. Using readFile
for
sending static files should not be necessary.
sendResponse
Sends a normal response with a 200 status code. This is really
just a convenience for when you need to break out of some deeply nested code
with an immediate response. Any instance of ToTypedContent
may be used.
sendWaiResponse
Used when you need to get low-level and send out a raw WAI response. This can be especially useful for creating streaming responses or for a technique like server-sent events.
The following functions allow you to generate various response headers:
setCookie
Sets a cookie on the client. Instead of taking an expiration date,
this function takes a cookie duration in minutes. Remember, you won’t see this
cookie using lookupCookie
until the following request.
deleteCookie
Tells the client to remove a cookie. Once again, lookupCookie
will not reflect this change until the next request.
setHeader
setLanguage
Sets the preferred user language, which will show up in the result
of the languages
function.
cacheSeconds
Sets a Cache-Control
header to indicate how many seconds this
response can be cached. This can be particularly useful if you are using
Varnish on your server.
neverExpires
Sets the Expires
header to the year 2037. You can use this for
content that should never expire, such as when the request path has a hash
value associated with it.
alreadyExpired
expiresAt
The HandlerT
and WidgetT
monad transformers are both instances of a number
of typeclasses. For this section, the important typeclasses are MonadIO
and
MonadLogger
. The former allows you to perform arbitrary IO
actions inside
your handler, such as reading from a file. In order to achieve this, you just
need to prepend liftIO
to the call.
MonadLogger
provides a built-in logging system. There are many ways you can
customize this system, including what messages get logged and where logs are
sent. By default, logs are sent to standard output. In development, all messages
are logged, and in production, warnings and errors are logged.
When logging, we often want to know where in the source code the logging
occurred. For this, MonadLogger
provides a number of convenience Template
Haskell functions that will automatically insert the source code location into the
log messages. These functions are $logDebug
, $logInfo
, $logWarn
, and
$logError
. Let’s look at a short example of some of these functions:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import
Control.Exception
(
IOException
,
try
)
import
Control.Monad
(
when
)
import
Yesod
data
App
=
App
instance
Yesod
App
where
-- This function controls which messages are logged
shouldLog
App
src
level
=
True
-- good for development
-- level == LevelWarn || level == LevelError -- good for production
mkYesod
"App"
[
parseRoutes
|
/
HomeR
GET
|
]
getHomeR
::
Handler
Html
getHomeR
=
do
$
logDebug
"Trying to read data file"
edata
<-
liftIO
$
try
$
readFile
"datafile.txt"
case
edata
::
Either
IOException
String
of
Left
e
->
do
$
logError
$
"Could not read datafile.txt"
defaultLayout
[
whamlet
|
An
error
occurred
|
]
Right
str
->
do
$
logInfo
"Reading of data file succeeded"
let
ls
=
lines
str
when
(
length
ls
<
5
)
$
$
logWarn
"Less than 5 lines of data"
defaultLayout
[
whamlet
|
<
ol
>
$
forall
l
<-
ls
<
li
>#
{
l
}
|
]
main
::
IO
()
main
=
warp
3000
App
We’ve seen a number of functions that work on URL-like things, such as redirect
. These functions all work with type-safe URLs, but what else do they work with? There’s a typeclass called RedirectUrl
that contains the logic for converting some type into a textual URL. This includes type-safe URLs, textual URLs, and two special instances:
A tuple of a URL and a list of key/value pairs of query string parameters
The Fragment
data type, used for adding a hash fragment to the end of a URL
Both of these instances allow you to “add on” extra information to a type-safe URL. Let’s see some examples of how these can be used:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import
Data.Set
(
member
)
import
Data.Text
(
Text
)
import
Yesod
import
Yesod.Auth
import
Yesod.Auth.Dummy
data
App
=
App
mkYesod
"App"
[
parseRoutes
|
/
HomeR
GET
/
link1
Link1R
GET
/
link2
Link2R
GET
/
link3
Link3R
GET
/
link4
Link4R
GET
|
]
instance
Yesod
App
where
getHomeR
::
Handler
Html
getHomeR
=
defaultLayout
$
do
setTitle
"Redirects"
[
whamlet
|
<
p
>
<
a
href
=@
{
Link1R
}
>
Click
to
start
the
redirect
loop
!
|
]
getLink1R
,
getLink2R
,
getLink3R
::
Handler
()
getLink1R
=
redirect
Link2R
-- /link1
getLink2R
=
redirect
(
Link3R
,
[(
"foo"
,
"bar"
)])
-- /link3?foo=bar
getLink3R
=
redirect
$
Link4R
:#:
(
"baz"
::
Text
)
-- /link4#baz
getLink4R
::
Handler
Html
getLink4R
=
defaultLayout
[
whamlet
|
<
p
>
You
made
it
!
|
]
main
::
IO
()
main
=
warp
3000
App
Of course, inside a Hamlet template this is usually not necessary, as you can simply include the hash after the URL directly. For example:
<a href=@{Link1R}#somehash>Link to hash
Routing and dispatch is arguably the core of Yesod: it is from here that our
type-safe URLs are defined, and the majority of our code is written within the
Handler
monad. This chapter covered some of the most important and central
concepts of Yesod, so it is important that you properly digest it.
This chapter also hinted at a number of more complex Yesod topics that we will be covering later, but you should be able to write some very sophisticated web applications with just the knowledge you have learned up until this point.