One of the stories from the early days of the Web is how search engines wiped out entire websites. When dynamic websites were still a new concept, developers didn’t appreciate the difference between a GET
and POST
request. As a result, they created pages—accessed with the GET
method—that would delete pages. When search engines started crawling these sites, they could wipe
out all the content.
If these web developers had followed the HTTP spec properly, this would not
have happened. A GET
request is supposed to cause no side effects (you know,
like wiping out a site). Recently, there has been a move in web development to
properly embrace representational state transfer (a.k.a. REST). This
chapter describes the RESTful features in Yesod and how you can use them to
create more robust web applications.
In many web frameworks, you write one handler function per resource. In Yesod,
the default is to have a separate handler function for each request method. The
two most common request methods you will deal with in creating websites are
GET
and POST
. These are the most well supported methods in HTML, as they
are the only ones supported by web forms. However, when creating RESTful APIs,
the other methods are very useful.
Technically speaking, you can create whichever request methods you like, but it is strongly recommended to stick to the ones spelled out in the HTTP spec. The most common of these are the following:
GET
Used for read-only requests. Assuming no other changes occur on the server,
calling a GET
request multiple times should result in the same response,
barring such things as “current time” or randomly assigned results.
POST
Used for general mutating requests. A POST
request should never be submitted
twice by the user. A common example of this would be to transfer funds from one
bank account to another.
PUT
Creates a new resource on the server, or replaces an existing one. It is safe to call this method multiple times.
DELETE
Just like it sounds: wipes out a resource on the server. Calling multiple times should be OK.
To a certain extent, this fits in very well with Haskell philosophy: a GET
request is similar to a pure function, which cannot have side effects. In
practice, your GET
functions will probably perform IO
, such as reading
information from a database, logging user actions, and so on.
See Chapter 7 for more information on the syntax of defining handler functions for each request method.
Suppose we have a Haskell data type and value:
data
Person
=
Person
{
name
::
String
,
age
::
Int
}
michael
=
Person
"Michael"
25
We could represent that data as HTML:
<table>
<tr>
<th>
Name</th>
<td>
Michael</td>
</tr>
<tr>
<th>
Age</th>
<td>
25</td>
</tr>
</table>
or we could represent it as JSON:
{
"name"
:
"Michael"
,
"age"
:
25
}
<person>
<name>
Michael</name>
<age>
25</age>
</person>
Web applications often use a different URL to get each of these representations—perhaps /person/michael.html, /person/michael.json, and so on. But Yesod follows the RESTful principle of a single URL for each resource, so in Yesod, all of these would be accessed from /person/michael.
Then the question becomes how we determine which representation to serve. The answer is the HTTP Accept
header: it gives a prioritized list of content types the client is expecting. Yesod provides a pair of functions to abstract away the details of parsing that header directly, and instead allows you to talk at a much higher level of representations. Let’s make that last sentence a bit more concrete with some code:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import
Data.Text
(
Text
)
import
Yesod
data
App
=
App
mkYesod
"App"
[
parseRoutes
|
/
HomeR
GET
|
]
instance
Yesod
App
getHomeR
::
Handler
TypedContent
getHomeR
=
selectRep
$
do
provideRep
$
return
[
shamlet
|
<
p
>
Hello
,
my
name
is
#
{
name
}
and
I
am
#
{
age
}
years
old
.
|
]
provideRep
$
return
$
object
[
"name"
.=
name
,
"age"
.=
age
]
where
name
=
"Michael"
::
Text
age
=
28
::
Int
main
::
IO
()
main
=
warp
3000
App
The selectRep
function says, “I’m about to give you some possible representations.” Each provideRep
call provides an alternative representation.
Yesod uses the Haskell types to determine the MIME type for each
representation. Because shamlet
(a.k.a., simple Hamlet) produces an Html
value, Yesod can determine that the relevant MIME type is text/html
.
Similarly, object
generates a JSON value, which implies the MIME type
application/json
. TypedContent
is a data type provided by Yesod for some
raw content with an attached MIME type. We’ll cover it in more detail in a
little bit.
To test this, start up the server and try running each of the following curl
commands:
curl http://localhost:3000 --header"accept: application/json"
curl http://localhost:3000 --header"accept: text/html"
curl http://localhost:3000
Notice how the response changes based on the Accept
header value. Also, when you leave off the header, the HTML response is displayed by default. The rule here is that if there is no Accept
header, the first representation is displayed. If an Accept
header is present, but we have no matches, then a 406 Not Acceptable response is returned.
By default, Yesod provides a convenience middleware that lets you set the Accept
header via a query string parameter. This can make it easier to test
from your browser. To try this out, you can visit http://localhost:3000/?_accept=application/json.
Because JSON is such a commonly used data format in web applications today, we
have some built-in helper functions for providing JSON representations. These
are built off of the wonderful aeson
library, so let’s start off with a quick
explanation of how that library works.
aeson
has a core data type, Value
, which represents any valid JSON value. It
also provides two typeclasses—ToJSON
and FromJSON
—to automate
marshaling to and from JSON values, respectively. For our purposes, we’re
currently interested in ToJSON
. Let’s look at a quick example of creating a
ToJSON
instance for our ever-recurring Person
data type examples:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
import
Data.Aeson
import
qualified
Data.ByteString.Lazy.Char8
as
L
import
Data.Text
(
Text
)
data
Person
=
Person
{
name
::
Text
,
age
::
Int
}
instance
ToJSON
Person
where
toJSON
Person
{
..
}
=
object
[
"name"
.=
name
,
"age"
.=
age
]
main
::
IO
()
main
=
L
.
putStrLn
$
encode
$
Person
"Michael"
28
I won’t go into further detail on aeson
, as
the Haddock documentation
already provides a great introduction to the library. What I’ve described so
far is enough to understand our convenience functions.
Let’s suppose that you have such a Person
data type, with a corresponding
value, and you’d like to use it as the representation for your current page.
For that, you can use the returnJson
function:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import
Data.Text
(
Text
)
import
Yesod
data
Person
=
Person
{
name
::
Text
,
age
::
Int
}
instance
ToJSON
Person
where
toJSON
Person
{
..
}
=
object
[
"name"
.=
name
,
"age"
.=
age
]
data
App
=
App
mkYesod
"App"
[
parseRoutes
|
/
HomeR
GET
|
]
instance
Yesod
App
getHomeR
::
Handler
Value
getHomeR
=
returnJson
$
Person
"Michael"
28
main
::
IO
()
main
=
warp
3000
App
returnJson
is actually a trivial function—it is implemented as return . toJSON
—but, it makes things just a bit more convenient. Similarly, if you
would like to provide a JSON value as a representation inside a selectRep
,
you can use provideJson
:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import
Data.Text
(
Text
)
import
Yesod
data
Person
=
Person
{
name
::
Text
,
age
::
Int
}
instance
ToJSON
Person
where
toJSON
Person
{
..
}
=
object
[
"name"
.=
name
,
"age"
.=
age
]
data
App
=
App
mkYesod
"App"
[
parseRoutes
|
/
HomeR
GET
|
]
instance
Yesod
App
getHomeR
::
Handler
TypedContent
getHomeR
=
selectRep
$
do
provideRep
$
return
[
shamlet
|
<
p
>
Hello
,
my
name
is
#
{
name
}
and
I
am
#
{
age
}
years
old
.
|
]
provideJson
person
where
person
@
Person
{
..
}
=
Person
"Michael"
28
main
::
IO
()
main
=
warp
3000
App
provideJson
is similarly trivial; in this case, it is implemented as provideRep . returnJson
.
Let’s say I’ve come up with some new data format based on using Haskell’s
Show
instance; I’ll call it “Haskell Show,” and give it a MIME type of
text/haskell-show
. And let’s say that I decide to include this representation
from my web app. How do I do it? For a first attempt, let’s use the
TypedContent
data type directly:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import
Data.Text
(
Text
)
import
Yesod
data
Person
=
Person
{
name
::
Text
,
age
::
Int
}
deriving
Show
data
App
=
App
mkYesod
"App"
[
parseRoutes
|
/
HomeR
GET
|
]
instance
Yesod
App
mimeType
::
ContentType
mimeType
=
"text/haskell-show"
getHomeR
::
Handler
TypedContent
getHomeR
=
return
$
TypedContent
mimeType
$
toContent
$
show
person
where
person
=
Person
"Michael"
28
main
::
IO
()
main
=
warp
3000
App
There are a few important things to note here:
We’ve used the toContent
function. This is a typeclass function that can
convert a number of data types to raw data ready to be sent over the wire. In
this case, we’ve used the instance for String
, which uses UTF8 encoding.
Other common data types with instances are Text
, ByteString
, Html
, and the aeson
library’s Value
.
We’re using the TypedContent
constructor directly. It takes two arguments: a MIME type and the raw content. Note that ContentType
is simply a type
alias for a strict ByteString
.
That’s all well and good, but it bothers me that the type signature for
getHomeR
is so uninformative. Also, the implementation of getHomeR
looks
pretty boilerplate. I’d rather just have a data type representing “Haskell Show”
data, and provide some simple means of creating such values. Let’s try this on
for size:
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import
Data.Text
(
Text
)
import
Yesod
data
Person
=
Person
{
name
::
Text
,
age
::
Int
}
deriving
Show
data
App
=
App
mkYesod
"App"
[
parseRoutes
|
/
HomeR
GET
|
]
instance
Yesod
App
mimeType
::
ContentType
mimeType
=
"text/haskell-show"
data
HaskellShow
=
forall
a
.
Show
a
=>
HaskellShow
a
instance
ToContent
HaskellShow
where
toContent
(
HaskellShow
x
)
=
toContent
$
show
x
instance
ToTypedContent
HaskellShow
where
toTypedContent
=
TypedContent
mimeType
.
toContent
getHomeR
::
Handler
HaskellShow
getHomeR
=
return
$
HaskellShow
person
where
person
=
Person
"Michael"
28
main
::
IO
()
main
=
warp
3000
App
The magic here lies in two typeclasses. As we mentioned before, ToContent
tells how to convert a value into a raw response. In our case, we would like to
show
the original value to get a String
, and then convert that String
into the raw content. Oftentimes, instances of ToContent
will build on each
other in this way.
ToTypedContent
is used internally by Yesod and is called on the result of
all handler functions. As you can see, the implementation is fairly trivial,
simply stating the MIME type and then calling out to toContent
.
Finally, let’s make this a bit more complicated and get it to play well with selectRep
:
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import
Data.Text
(
Text
)
import
Yesod
data
Person
=
Person
{
name
::
Text
,
age
::
Int
}
deriving
Show
instance
ToJSON
Person
where
toJSON
Person
{
..
}
=
object
[
"name"
.=
name
,
"age"
.=
age
]
data
App
=
App
mkYesod
"App"
[
parseRoutes
|
/
HomeR
GET
|
]
instance
Yesod
App
mimeType
::
ContentType
mimeType
=
"text/haskell-show"
data
HaskellShow
=
forall
a
.
Show
a
=>
HaskellShow
a
instance
ToContent
HaskellShow
where
toContent
(
HaskellShow
x
)
=
toContent
$
show
x
instance
ToTypedContent
HaskellShow
where
toTypedContent
=
TypedContent
mimeType
.
toContent
instance
HasContentType
HaskellShow
where
getContentType
_
=
mimeType
getHomeR
::
Handler
TypedContent
getHomeR
=
selectRep
$
do
provideRep
$
return
$
HaskellShow
person
provideJson
person
where
person
=
Person
"Michael"
28
main
::
IO
()
main
=
warp
3000
App
The important addition here is the HasContentType
instance. This may seem
redundant, but it serves an important role. We need to be able to determine the MIME type of a possible representation before creating that representation.
ToTypedContent
only works on a concrete value, and therefore can’t be used
before creating the value. getContentType
instead takes a proxy value,
indicating the type without providing anything concrete.
There are a great deal of other request headers available. Some of them only
affect the transfer of data between the server and client, and should not
affect the application at all. For example, Accept-Encoding
informs the
server which compression schemes the client understands, and Host
informs the
server which virtual host to serve up.
Other headers do affect the application, but are automatically read by Yesod.
For example, the Accept-Language
header specifies which human language
(English, Spanish, German, Swiss-German) the client prefers. See Chapter 22 for details on how this header is used.
Yesod adheres to the following tenets of REST:
Use the correct request method.
Each resource should have precisely one URL.
Allow multiple representations of data on the same URL.
Inspect request headers to determine extra information about what the client wants.
This makes it easy to use Yesod not just for building websites, but for
building APIs. In fact, using techniques such as selectRep
/provideRep
, you
can serve both a user-friendly HTML page and a machine-friendly JSON page
from the same URL.