loading...
Cover image for How to write a Haskell web service (from scratch) - Part 3

How to write a Haskell web service (from scratch) - Part 3

parambirs profile image Parambir Singh ・5 min read

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.

Posted on by:

Discussion

markdown guide
 

Thanks a lot!!! Long life to Haskell Web Development!!!