This is the third part of a hands-on guide on creating a Haskell based web service. No previous knowledge of haskell is required. However, basic understanding of RESTful web services is assumed. Other posts in this series: part 1, part 2.
3. Run a basic Haskell web app using Scotty
Introducing Scotty
There are many web frameworks available for Haskell: Yesod, Snap, Scotty, etc. I chose Scotty over others as it seemed easier to get started with.
We’ll write a simple web service that responds to various HTTP request types (GET, PUT, POST, DELETE). We’ll see how to get request headers, path parameters and form fields and how to respond with plain-text, html or JSON response.
Initialise Cabal
Let’s initialise a cabal app for our web service. I’ve mostly chosen the default options. Two notable exceptions include:
license: 9) MIT
source directory: 2) server
% mkdir scotty-webapp-example
% cd scotty-webapp-example
% cabal sandbox init
Writing a default package environment file to
/Users/psingh/tmp/haskell/eac-articles/scotty-webapp-example/cabal.sandbox.config
Using an existing sandbox located at
/Users/psingh/tmp/haskell/eac-articles/scotty-webapp-example/.cabal-sandbox
% cabal init
Package name? [default: scotty-webapp-example]
Package version? [default: 0.1.0.0]
...
...
Write the server code
Since we told cabal
earlier that our main module for the executable will be Main.hs
, and that it will live inside the server
folder, let’s add server/Main.hs
file to our source.
{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty
import Network.HTTP.Types
main = scotty 3000 $ do
get "/" $ do -- handle GET request on "/" URL
text "This was a GET request!" -- send 'text/plain' response
delete "/" $ do
html "This was a DELETE request!" -- send 'text/html' response
post "/" $ do
text "This was a POST request!"
put "/" $ do
text "This was a PUT request!"
As you can probably figure out, this code will start a server on port 3000 and:
- For a
GET
request on the/
path, the server will respond with an HTML response with the content “This was a GET request!” - For a
DELETE
request on the/
path, the server will respond with a ‘plain-text’ response with the content “This was a DELETE request!”
Add dependencies
Let’s add a dependency for scotty
and http-types
libraries in our cabal file.
This is how the build-depends segment of the scotty-webapp-example.cabal files looks right now:
build-depends: base >=4.8 && <4.9
Change it to:
build-depends: base >=4.8 && <4.9
, scotty
, http-types
Next, run cabal install
to add the dependencies into your sandbox followed by cabal run
to run the server.
% cabal install
Resolving dependencies…
Notice: installing into a sandbox located at
/Users/psingh/tmp/haskell/eac-articles/scotty-webapp-example/.cabal-sandbox
Configuring ansi-terminal-0.6.2.3…
Configuring appar-0.1.4…
…
…
% cabal run
Package has never been configured. Configuring with default flags. If this fails, please run configure manually.
Resolving dependencies…
Configuring scotty-webapp-example-0.1.0.0…
Preprocessing executable ‘scotty-webapp-example’ for
scotty-webapp-example-0.1.0.0…
[1 of 1] Compiling Main ( server/Main.hs, dist/build/scotty-webapp-example/scotty-webapp-example-tmp/Main.o )
Linking dist/build/scotty-webapp-example/scotty-webapp-example …
Running scotty-webapp-example…
Setting phasers to stun… (port 3000) (ctrl-c to quit)
The server will be running now on port 3000. Let’s verify that (using the excellent http tool:
% http delete :3000
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Date: Mon, 14 Sep 2015 05:44:57 GMT
Server: Warp/3.1.3
Transfer-Encoding: chunked
This was a DELETE request!
Great!
Handling more complex requests
Let’s add a few more handlers to handle different kinds of requests. Add the following to server/Main.hs
:
-- set a header:
post "/set-headers" $ do
status status302 -- Respond with HTTP 302 status code
setHeader "Location" "http://www.google.com.au"
-- named parameters:
get "/askfor/:word" $ do
w <- param "word"
html $ mconcat ["<h1>You asked for ", w, ", you got it!</h1>" ]
-- unnamed parameters from a query string or a form:
post "/submit" $ do -- e.g. http://server.com/submit?name=somename
name <- param "name"
text name
-- match a route regardless of the method
matchAny "/all" $ do
text "matches all methods"
-- handler for when there is no matched route
-- (this should be the last handler because it matches all routes)
notFound $ do
text "there is no such route."
Encode/Decode JSON
Most web services these days interact via JSON. Haskell provides a type safe way to encode/decode JSON strings using the Aeson library.
Defining the model
Let’s create an Article
data type that could represent a news article for example. An article consists of 3 fields: anInteger id, a Text title and a Text bodyText. By making Article an instance of FromJSON
and ToJSON
typeclasses, we can use Aeson library for converting between JSON strings and Article objects. Add the following code to the file server/Article.hs
:
{-# LANGUAGE OverloadedStrings #-}
module Article where
import Data.Text.Lazy
import Data.Text.Lazy.Encoding
import Data.Aeson
import Control.Applicative
-- Define the Article constructor
-- e.g. Article 12 "some title" "some body text"
data Article = Article Integer Text Text -- id title bodyText
deriving (Show)
-- Tell Aeson how to create an Article object from JSON string.
instance FromJSON Article where
parseJSON (Object v) = Article <$>
v .:? "id" .!= 0 <*> -- the field "id" is optional
v .: "title" <*>
v .: "bodyText"
-- Tell Aeson how to convert an Article object to a JSON string.
instance ToJSON Article where
toJSON (Article id title bodyText) =
object ["id" .= id,
"title" .= title,
"bodyText" .= bodyText]
We’ll need to add a couple of routes to our Scotty router function to handle encoding and decoding Article types:
main = scotty 3000 $ do
-- get article (json)
get "/article" $ do
json $ Article 13 "caption" "content" -- Call Article constructor and encode the result as JSON
-- post article (json)
post "/article" $ do
article <- jsonData :: ActionM Article -- Decode body of the POST request as an Article object
json article -- Send the encoded object back as JSON
We’ll also need to add a couple of dependencies to scotty-webapp-example.cabal
file:
build-depends: base >=4.8 && <4.9
, scotty
, http-types
, text
, aeson
Test JSON encoding/decoding
Let’s fire up Scotty and see if it can handle JSON properly:
GET /article
% http get :3000/article
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 14 Sep 2015 06:51:17 GMT
Server: Warp/3.1.3
Transfer-Encoding: chunked
{
“bodyText”: “content”,
“id”: 13,
“title”: “caption”
}
POST /article
% http post :3000/article id:=23 title=”new caption” bodyText=”some content”
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 14 Sep 2015 06:56:57 GMT
Server: Warp/3.1.3
Transfer-Encoding: chunked
{
“bodyText”: “some content”,
“id”: 23,
“title”: “new caption”
}
Source
The complete source for this section is available on github
Finally
I hope you found this tutorial useful. Please let me know if something didn’t work for you or if I missed documenting any step.
Top comments (1)
Thanks a lot!!! Long life to Haskell Web Development!!!