RESTful Content
One of the stories from the early days of the web is how search engines wiped out entire websites. When dynamic web sites 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, also known as REST. This chapter describes the RESTful features in Yesod and how you can use them to create more robust web applications.
Request methods
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 web sites are GET
and POST
. These are the most well-supported methods in HTML, since 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:
GET
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
A general mutating request. 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
Create a new resource on the server, or replace an existing one. This method is safe to be called multiple times.
PATCH
Updates the resource partially on the server. When you want to update one or more field of the resource, this method should be preferred.
DELETE
Just like it sounds: wipe 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 the routing and handlers chapter for more information on the syntax of defining handler functions for each request method.
Representations
Suppose we have a Haskell datatype 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}
or as XML:
<person>
<name>Michael</name>
<age>25</age>
</person>
Often times, web applications will use a different URL to get each of these representations; perhaps /person/michael.html
, /person/michael.json
, etc. 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 do 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 alternate representation. Yesod uses the Haskell types to determine the mime type for each representation. Since 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 out, start up the server and then try running the following different 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.
JSON conveniences
Since 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 datatype, 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
datatype, 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
. However, 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 provideRep . returnJson
.
New datatypes
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
datatype 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 forString
, which uses UTF8 encoding. Other common data types with instances areText
,ByteString
,Html
, and aeson’sValue
.We’re using the
TypedContent
constructor directly. It takes two arguments: a mime type, and the raw content. Note thatContentType
is simply a type alias for a strictByteString
.
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 datatype 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. Often times, 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 this 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.
If you want to provide a representation for a value that doesn’t have a HasContentType
instance, you can use the provideRepType
function, which requires you to explicitly state the mime type present.
Other request headers
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 the i18n chapter for details on how this header is used.
Summary
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.