Rest api in Haskell

Recently I’ve discovered world of functional programming and was really surprised by the amount of fun which it gives back. Not without a reason Haskell is the king of weekend programming:

haskelltags.png
Source: Stackoverflow blog

I want to show you how to build simple rest api with Haskell.

Preparing

You will need:

Execution

run:

1
stack new projectname

to set up all files needed by stack and go to that folder.

Then in projectname.cabal you need to add some libraries. Find executable projectname and add:

1
2
3
4
5
6
7
8
9
executable projectname
    hs-source-dirs: ... #what you want
    main-is: Main.hs
    other-extensions: OverloadedStrings
    build-depends:  base,
                    scotty,
                    aeson,
                    postgresql-simple
    default-language: Haskell2010

then run:

1
stack build

to install all dependencies. Move to folder with Main.sh and let’s start programming!

Haskell time!

Firstly, let’s make super simple api to check if everything is correct:

1
2
3
4
5
6
7
8
9
10
11
12
13
{-# LANGUAGE OverloadedStrings #-}
module Main where

import Web.Scotty

server :: ScottyM ()
server = do
    get "/alive" $ do
        text "yep!"

main :: IO ()
main = do
    scotty 1234 server

and run:

1
2
stack build
stack exec projectname

and now check it in your browser! Write in url: localhost:1234/alive and voilà!

(You can install stack run for less typing when you want to build and execute your program!)

Database setting up

Ok, let’s connect to the database! If you have problem with setting up your Postgresql database I recomend visiting DigitalOcean tutorial

I prepared a simple database input for tutorial purposes. It’s gonna be a simple todo list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE "checklists" (
      "id" SERIAL PRIMARY KEY,
      "title" TEXT
);

insert into checklists (title) values
('backend'),
('shopping');

CREATE TABLE "checklistitems" (
      "id" SERIAL PRIMARY KEY,
      "name" TEXT NOT NULL,
      "finished" BOOLEAN NOT NULL,
      "checklist" INTEGER NOT NULL
);

insert into checklistitems (name, finished, checklist) values
('reformat code', true, 1),
('user login', true, 1),
('add CI', false, 1),
('tomato', false, 2),
('potato', false, 2);

Connect to database

To do this, we need to import PostgreSQL.Simple library

1
import Database.PostgreSQL.Simple

and make a connection:

1
conn <- connectPostgreSQL ("host='127.0.0.1' user='haxor' dbname='haxordb' password='pass'")

Now we should create Checklist and ChecklistItem classes:

1
2
3
4
5
6
7
{-# LANGUAGE DeriveGeneric #-}

...

data Checklist = Checklist { checklistId :: Maybe Int,
    title :: String,
    checklistItems :: [ChecklistItem]} deriving (Show, Generic)

Simple class definition. We use DeriveGeneric for “generic” programming. In next lines of code it will become handy.

1
2
3
4
5
instance FromRow Checklist where
    fromRow = Checklist <$> field <*> field <*> pure []

instance ToRow Checklist where
    toRow c = [toField $ title c]

In case of creating object from SQL query we need to list all fields of our class. Haskell won’t let us create object without setting up all fields. But we cannot get checklist and checklist items in one single query, so we need to pass pure [] as our checklistItems list. When we want to make oposite: include our object into SQL queries - we have full control about which fields we want to pass.

1
2
instance ToJSON Checklist
instance FromJSON Checklist

Here we use {-# LANGUAGE DeriveGeneric #-} language extension. GHC implements this for us.

The same story goes for ChecklistItem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data ChecklistItem = ChecklistItem { checklistItemId :: Maybe Int,
    itemText :: String,
    finished :: Bool,
    checklist :: Int } deriving (Show, Generic)

instance FromRow ChecklistItem where
    fromRow = ChecklistItem <$> field <*> field <*> field <*> field

instance ToRow ChecklistItem where
    toRow i = [toField $ itemText i, toField $ finished i, toField $ checklist i]

instance ToJSON ChecklistItem

instance FromJSON ChecklistItem

Database queries

Ok, let’s do some queries to our server!

1
2
3
4
5
6
7
8
9
10
11
12
import Control.Monad.IO.Class

server :: Connection -> ScottyM()
server conn = do
    get "/checklists" $ do
        checklists <- liftIO (query_ conn "select id, title from checklists" :: IO [Checklist])
        json checklists

main :: IO ()
main = do
    conn <- connectPostgreSQL ("host='127.0.0.1' user='blog' dbname='blog' password='pass'")
    scotty 1234 $ server conn

It seems working, but we get Checklists without items. How to fix it?

1
2
3
4
5
6
7
8
9
10
11
12
server :: Connection -> ScottyM()
server conn = do
    get "/checklists" $ do
        checklists <- liftIO (query_ conn "select id, title from checklists" :: IO [Checklist])
        checkWithItems <- liftIO (mapM (setArray conn) checklists)
        json checkWithItems

setArray :: Connection -> Checklist -> IO Checklist
setArray conn check = do
    let queryText = "select id, name, finished, checklist from checklistitems where checklist = (?)"
    items <- liftIO (query conn queryText (Only $ checklistId check) :: IO [ChecklistItem])
    return check { checklistItems = items }

Notice that this time we use query instead of query_. query makes “query substitution”: it takes ToRow instance and inserts values in places of ‘?’ inside query string.

Now when we hit localhost:1234/checklists it will return:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
[
  {
    "checklistItems": [
      {
        "checklist": 1,
        "checklistItemId": 1,
        "finished": true,
        "itemText": "reformat code"
      },
      {
        "checklist": 1,
        "checklistItemId": 2,
        "finished": true,
        "itemText": "user login"
      },
      {
        "checklist": 1,
        "checklistItemId": 3,
        "finished": false,
        "itemText": "add CI"
      }
    ],
    "checklistId": 1,
    "title": "backend"
  },
  {
    "checklistItems": [
      {
        "checklist": 2,
        "checklistItemId": 4,
        "finished": false,
        "itemText": "tomato"
      },
      {
        "checklist": 2,
        "checklistItemId": 5,
        "finished": false,
        "itemText": "potato"
      }
    ],
    "checklistId": 2,
    "title": "shopping"
  }
]

Now let’s implement post method for checklist items:

1
2
3
4
5
6
7
8
9
10
11
12
server conn = do
    ...
    post "/checklistitems" $ do
        item <- jsonData :: ActionM ChecklistItem
        newItem <- liftIO (insertChecklist conn item)
        json newItem

insertChecklist :: Connection -> ChecklistItem -> IO ChecklistItem
insertChecklist conn item = do
    let insertQuery = "insert into checklistitems (name, finished, checklist) values (?, ?, ?) returning id"
    [Only id] <- query conn insertQuery item
    return item { checklistItemId = id }

Now we can create new ChecklistItem and our api returning new object with assigned id.

Sum up

Ok, we’ve made a simple json rest api in Haskell with get and post methods! Here is a full source:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
module Main where

import Web.Scotty
import Data.Aeson (FromJSON, ToJSON)
import Database.PostgreSQL.Simple
import Database.PostgreSQL.Simple.ToRow
import Database.PostgreSQL.Simple.FromRow
import Database.PostgreSQL.Simple.ToField
import GHC.Generics
import Control.Monad.IO.Class

server :: Connection -> ScottyM()
server conn = do
    get "/checklists" $ do
        checklists <- liftIO (query_ conn "select id, title from checklists" :: IO [Checklist])
        checkWithItems <- liftIO (mapM (setArray conn) checklists)
        json checkWithItems
    post "/checklistitems" $ do
        item <- jsonData :: ActionM ChecklistItem
        newItem <- liftIO (insertChecklist conn item)
        json newItem

selectChecklistQuery = "select id, name, finished, checklist from checklistitems where checklist = (?)"
insertItemsQuery = "insert into checklistitems (name, finished, checklist) values (?, ?, ?) returning id"

setArray :: Connection -> Checklist -> IO Checklist
setArray conn check = do
    items <- liftIO (query conn selectChecklistQuery (Only $ checklistId check) :: IO [ChecklistItem])
    return check { checklistItems = items }

insertChecklist :: Connection -> ChecklistItem -> IO ChecklistItem
insertChecklist conn item = do
    [Only id] <- query conn insertItemsQuery item
    return item { checklistItemId = id }

main :: IO ()
main = do
    conn <- connectPostgreSQL ("host='127.0.0.1' user='blog' dbname='blog' password='pass'")
    scotty 1234 $ server conn



data Checklist = Checklist { checklistId :: Maybe Int,
    title :: String,
    checklistItems :: [ChecklistItem]} deriving (Show, Generic)

instance FromRow Checklist where
    fromRow = Checklist <$> field <*> field <*> pure []
instance ToRow Checklist where
    toRow c = [toField $ title c]
instance ToJSON Checklist
instance FromJSON Checklist

data ChecklistItem = ChecklistItem { checklistItemId :: Maybe Int,
    itemText :: String,
    finished :: Bool,
    checklist :: Int } deriving (Show, Generic)

instance FromRow ChecklistItem where
    fromRow = ChecklistItem <$> field <*> field <*> field <*> field
instance ToRow ChecklistItem where
    toRow i = [toField $ itemText i, toField $ finished i, toField $ checklist i]
instance ToJSON ChecklistItem
instance FromJSON ChecklistItem

If you have some questions feel free to leave a comment!