Writing a Simple REST Web Service in PureScript - Part 2

21 minute read

To recap, in the first part of this two-part tutorial, we built a simple JSON REST web service in PureScript to create, update, get, list and delete users, backed by a Postgres database. In this part we’ll work on the rest of the requirements:

  1. validation of API requests.
  2. reading the server and database configs from environment variables.
  3. logging HTTP requests and debugging info.

But first,

Bugs!

What happens if we hit a URL on our server which does not exist? Let’s fire up the server and test it:

$ pulp --watch run
$ http GET http://localhost:4000/v1/random
HTTP/1.1 404 Not Found
Connection: keep-alive
Content-Length: 148
Content-Security-Policy: default-src 'self'
Content-Type: text/html; charset=utf-8
Date: Sat, 30 Sep 2017 08:23:20 GMT
X-Content-Type-Options: nosniff
X-Powered-By: Express

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /v1/random</pre>
</body>
</html>

We get back a default HTML response with a 404 status from Express. Since we are writing a JSON API, we should return a JSON response in this case too. We add the following code in the src/SimpleService/Server.purs file to add a catch-all route and send a 404 status with a JSON error message:

-- previous code
import Data.Either (fromRight)
import Data.String.Regex (Regex, regex) as Re
import Data.String.Regex.Flags (noFlags) as Re
import Node.Express.App (App, all, delete, get, http, listenHttp, post, useExternal)
import Node.Express.Response (sendJson, setStatus)
import Partial.Unsafe (unsafePartial)
-- previous code

allRoutePattern :: Re.Regex
allRoutePattern = unsafePartial $ fromRight $ Re.regex "/.*" Re.noFlags

app :: forall eff. PG.Pool -> App (postgreSQL :: PG.POSTGRESQL | eff)
app pool = do
  useExternal jsonBodyParser

  get "/v1/user/:id"    $ getUser pool
  delete "/v1/user/:id" $ deleteUser pool
  post "/v1/users"      $ createUser pool
  patch "/v1/user/:id"  $ updateUser pool
  get "/v1/users"       $ listUsers pool

  all allRoutePattern do
    setStatus 404
    sendJson {error: "Route not found"}
  where
    patch = http (CustomMethod "patch")

allRoutePattern matches all routes because it uses a "/.*" regular expression. We place it as the last route to match all the otherwise unrouted requests. Let’s see what is the result:

$ http GET http://localhost:4000/v1/random
HTTP/1.1 404 Not Found
Connection: keep-alive
Content-Length: 27
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 08:46:46 GMT
ETag: W/"1b-772e0u4nrE48ogbR0KmKfSvrHUE"
X-Powered-By: Express

{
    "error": "Route not found"
}

Now we get a nicely formatted JSON response.

Another scenario is when our application throws some uncaught error. To simulate this, we shut down our postgres database and hit the server for listing users:

$ http GET http://localhost:4000/v1/users
HTTP/1.1 500 Internal Server Error
Connection: keep-alive
Content-Length: 372
Content-Security-Policy: default-src 'self'
Content-Type: text/html; charset=utf-8
Date: Sat, 30 Sep 2017 08:53:40 GMT
X-Content-Type-Options: nosniff
X-Powered-By: Express

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: connect ECONNREFUSED 127.0.0.1:5432<br> &nbsp; &nbsp;at Object._errnoException (util.js:1026:11)<br> &nbsp; &nbsp;at _exceptionWithHostPort (util.js:1049:20)<br> &nbsp; &nbsp;at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1174:14)</pre>
</body>
</html>

We get another default HTML response from Express with a 500 status. Again, in this case we’d like to return a JSON response. We add the following code to the src/SimpleService/Server.purs file:

-- previous code
import Control.Monad.Eff.Exception (message)
import Node.Express.App (App, all, delete, get, http, listenHttp, post, useExternal, useOnError)
-- previous code

app :: forall eff. PG.Pool -> App (postgreSQL :: PG.POSTGRESQL | eff)
app pool = do
  -- previous code
  useOnError \err -> do
    setStatus 500
    sendJson {error: message err}
  where
    patch = http (CustomMethod "patch")

We add the useOnError handler which comes with purescript-express to return the error message as a JSON response. Back on the command-line:

$ http GET http://localhost:4000/v1/users
HTTP/1.1 500 Internal Server Error
Connection: keep-alive
Content-Length: 47
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 09:01:37 GMT
ETag: W/"2f-cJuIW6961YCpo9TWDSZ9VWHLGHE"
X-Powered-By: Express

{
    "error": "connect ECONNREFUSED 127.0.0.1:5432"
}

It works! Bugs are fixed now. We proceed to add next features.

Validation

Let’s recall the code to update a user from the src/SimpleService/Handler.purs file:

updateUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
updateUser pool = getRouteParam "id" >>= case _ of
  Nothing -> respond 422 { error: "User ID is required" }
  Just sUserId -> case fromString sUserId of
    Nothing -> respond 422 { error: "User ID must be positive: " <> sUserId }
    Just userId -> getBody >>= case _ of
      Left errs -> respond 422 { error: intercalate ", " $ map renderForeignError errs}
      Right (UserPatch userPatch) -> case unNullOrUndefined userPatch.name of
        Nothing -> respondNoContent 204
        Just userName -> if userName == ""
          then respond 422 { error: "User name must not be empty" }
          else do
            savedUser <- liftAff $ PG.withConnection pool \conn -> PG.withTransaction conn do
              P.findUser conn userId >>= case _ of
                Nothing -> pure Nothing
                Just (User user) -> do
                  let user' = User (user { name = userName })
                  P.updateUser conn user'
                  pure $ Just user'
            case savedUser of
              Nothing -> respond 404 { error: "User not found with id: " <> sUserId }
              Just user -> respond 200 (encode user)

As we can see, the actual request handling logic is obfuscated by the request validation logic for the user id and the user name patch parameters. We also notice that we are using three constructs for validation here: Maybe, Either and if-then-else. However, we can use just Either to subsume all these cases as it can “carry” a failure as well as a success case. Either also comes with a nice monad transformer ExceptT which provides the do syntax for failure propagation. So we choose ExceptT as the base construct for our validation framework and write functions to upgrade Maybe and if-then-else to it. We add the following code to the src/SimpleService/Validation.purs file:

module SimpleService.Validation
  (module MoreExports, module SimpleService.Validation) where

import Prelude

import Control.Monad.Except (ExceptT, except, runExceptT)
import Data.Either (Either(..))
import Data.Maybe (Maybe(..))
import Node.Express.Handler (HandlerM, Handler)
import Node.Express.Response (sendJson, setStatus)
import Node.Express.Types (EXPRESS)
import Control.Monad.Except (except) as MoreExports

type Validation eff a = ExceptT String (HandlerM (express :: EXPRESS | eff)) a

exceptMaybe :: forall e m a. Applicative m => e -> Maybe a -> ExceptT e m a
exceptMaybe e a = except $ case a of
  Just x  -> Right x
  Nothing -> Left e

exceptCond :: forall e m a. Applicative m => e -> (a -> Boolean) -> a -> ExceptT e m a
exceptCond e cond a = except $ if cond a then Right a else Left e

withValidation :: forall eff a. Validation eff a -> (a -> Handler eff) -> Handler eff
withValidation action handler = runExceptT action >>= case _ of
  Left err -> do
    setStatus 422
    sendJson {error: err}
  Right x  -> handler x

We re-export except from the Control.Monad.Except module. We also add a withValidation function which runs an ExceptT based validation and either returns an error response with a 422 status in case of a failed validation or runs the given action with the valid value in case of a successful validation.

Using these functions, we now write updateUser in the src/SimpleService/Handler.purs file as:

-- previous code
import Control.Monad.Trans.Class (lift)
import Data.Bifunctor (lmap)
import Data.Foreign (ForeignError, renderForeignError)
import Data.List.NonEmpty (toList)
import Data.List.Types (NonEmptyList)
import Data.Tuple (Tuple(..))
import SimpleService.Validation as V
-- previous code

renderForeignErrors :: forall a. Either (NonEmptyList ForeignError) a -> Either String a
renderForeignErrors = lmap (toList >>> map renderForeignError >>> intercalate ", ")

updateUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
updateUser pool = V.withValidation (Tuple <$> getUserId <*> getUserPatch)
                                   \(Tuple userId (UserPatch userPatch)) ->
    case unNullOrUndefined userPatch.name of
      Nothing -> respondNoContent 204
      Just uName -> V.withValidation (getUserName uName) \userName -> do
        savedUser <- liftAff $ PG.withConnection pool \conn -> PG.withTransaction conn do
          P.findUser conn userId >>= case _ of
            Nothing -> pure Nothing
            Just (User user) -> do
              let user' = User (user { name = userName })
              P.updateUser conn user'
              pure $ Just user'
        case savedUser of
          Nothing -> respond 404 { error: "User not found with id: " <> show userId }
          Just user -> respond 200 (encode user)
  where
    getUserId = lift (getRouteParam "id")
      >>= V.exceptMaybe "User ID is required"
      >>= fromString >>> V.exceptMaybe "User ID must be positive"

    getUserPatch = lift getBody >>= V.except <<< renderForeignErrors

    getUserName = V.exceptCond "User name must not be empty" (_ == "")

The validation logic has been extracted out in separate functions now which are composed using Applicative. The validation steps are composed using the ExceptT monad. We are now free to express the core logic of the function clearly. We rewrite the src/SimpleService/Handler.purs file using the validations:

module SimpleService.Handler where

import Prelude

import Control.Monad.Aff.Class (liftAff)
import Control.Monad.Trans.Class (lift)
import Data.Bifunctor (lmap)
import Data.Either (Either)
import Data.Foldable (intercalate)
import Data.Foreign (ForeignError, renderForeignError)
import Data.Foreign.Class (encode)
import Data.Foreign.NullOrUndefined (unNullOrUndefined)
import Data.Int (fromString)
import Data.List.NonEmpty (toList)
import Data.List.Types (NonEmptyList)
import Data.Maybe (Maybe(..))
import Data.Tuple (Tuple(..))
import Database.PostgreSQL as PG
import Node.Express.Handler (Handler)
import Node.Express.Request (getBody, getRouteParam)
import Node.Express.Response (end, sendJson, setStatus)
import SimpleService.Persistence as P
import SimpleService.Validation as V
import SimpleService.Types

getUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
getUser pool = V.withValidation getUserId \userId ->
  liftAff (PG.withConnection pool $ flip P.findUser userId) >>= case _ of
    Nothing -> respond 404 { error: "User not found with id: " <> show userId }
    Just user -> respond 200 (encode user)

deleteUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
deleteUser pool = V.withValidation getUserId \userId -> do
  found <- liftAff $ PG.withConnection pool \conn -> PG.withTransaction conn do
    P.findUser conn userId >>= case _ of
      Nothing -> pure false
      Just _  -> do
        P.deleteUser conn userId
        pure true
  if found
    then respondNoContent 204
    else respond 404 { error: "User not found with id: " <> show userId }

createUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
createUser pool = V.withValidation getUser \user@(User _) -> do
  liftAff (PG.withConnection pool $ flip P.insertUser user)
  respondNoContent 201
  where
    getUser = lift getBody
      >>= V.except <<< renderForeignErrors
      >>= V.exceptCond "User ID must be positive" (\(User user) -> user.id > 0)
      >>= V.exceptCond "User name must not be empty" (\(User user) -> user.name /= "")

updateUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
updateUser pool = V.withValidation (Tuple <$> getUserId <*> getUserPatch)
                                   \(Tuple userId (UserPatch userPatch)) ->
    case unNullOrUndefined userPatch.name of
      Nothing -> respondNoContent 204
      Just uName -> V.withValidation (getUserName uName) \userName -> do
        savedUser <- liftAff $ PG.withConnection pool \conn -> PG.withTransaction conn do
          P.findUser conn userId >>= case _ of
            Nothing -> pure Nothing
            Just (User user) -> do
              let user' = User (user { name = userName })
              P.updateUser conn user'
              pure $ Just user'
        case savedUser of
          Nothing -> respond 404 { error: "User not found with id: " <> show userId }
          Just user -> respond 200 (encode user)
  where
    getUserPatch = lift getBody >>= V.except <<< renderForeignErrors
    getUserName = V.exceptCond "User name must not be empty" (_ /= "")

listUsers :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
listUsers pool = liftAff (PG.withConnection pool P.listUsers) >>= encode >>> respond 200

getUserId :: forall eff. V.Validation eff Int
getUserId = lift (getRouteParam "id")
  >>= V.exceptMaybe "User ID is required"
  >>= fromString >>> V.exceptMaybe "User ID must be an integer"
  >>= V.exceptCond "User ID must be positive" (_ > 0)

renderForeignErrors :: forall a. Either (NonEmptyList ForeignError) a -> Either String a
renderForeignErrors = lmap (toList >>> map renderForeignError >>> intercalate ", ")

respond :: forall eff a. Int -> a -> Handler eff
respond status body = do
  setStatus status
  sendJson body

respondNoContent :: forall eff. Int -> Handler eff
respondNoContent status = do
  setStatus status
  end

The code is much cleaner now. Let’s try out a few test cases:

$ http POST http://localhost:4000/v1/users id:=3 name=roger
HTTP/1.1 201 Created
Connection: keep-alive
Content-Length: 0
Date: Sat, 30 Sep 2017 12:13:37 GMT
X-Powered-By: Express
$ http POST http://localhost:4000/v1/users id:=3
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 102
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 12:13:50 GMT
ETag: W/"66-/c4cfoquQZGwtDBUzHjJydJAHJ0"
X-Powered-By: Express

{
    "error": "Error at array index 0: (ErrorAtProperty \"name\" (TypeMismatch \"String\" \"Undefined\"))"
}
$ http POST http://localhost:4000/v1/users id:=3 name=""
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 39
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 12:14:02 GMT
ETag: W/"27-JQsh12xu/rEFdWy8REF4NMtBUB4"
X-Powered-By: Express

{
    "error": "User name must not be empty"
}
$ http POST http://localhost:4000/v1/users id:=0 name=roger
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 36
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 12:14:14 GMT
ETag: W/"24-Pvt1L4eGilBmVtaOGHlSReJ413E"
X-Powered-By: Express

{
    "error": "User ID must be positive"
}
$ http GET http://localhost:4000/v1/user/3
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 23
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 12:14:28 GMT
ETag: W/"17-1scpiB1FT9DBu9s4I1gNWSjH2go"
X-Powered-By: Express

{
    "id": 3,
    "name": "roger"
}
$ http GET http://localhost:4000/v1/user/asdf
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 38
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 12:14:40 GMT
ETag: W/"26-//tvORl1gGDUMwgSaqbEpJhuadI"
X-Powered-By: Express

{
    "error": "User ID must be an integer"
}
$ http GET http://localhost:4000/v1/user/-1
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 36
Content-Type: application/json; charset=utf-8
Date: Sat, 30 Sep 2017 12:14:45 GMT
ETag: W/"24-Pvt1L4eGilBmVtaOGHlSReJ413E"
X-Powered-By: Express

{
    "error": "User ID must be positive"
}

It works as expected.

Configuration

Right now our application configuration resides in the main function:

main = runServer port databaseConfig
  where
    port = 4000
    databaseConfig = { user: "abhinav"
                     , password: ""
                     , host: "localhost"
                     , port: 5432
                     , database: "simple_service"
                     , max: 10
                     , idleTimeoutMillis: 1000
                     }

We are going to extract it out of the code and read it from the environment variables using the purescript-config package. First, we install the required packages using bower.

$ bower install --save purescript-node-process purescript-config

Now, we write the following code in the src/SimpleService/Config.purs file:

module SimpleService.Config where

import Data.Config
import Prelude

import Control.Monad.Eff (Eff)
import Data.Config.Node (fromEnv)
import Data.Either (Either)
import Data.Set (Set)
import Database.PostgreSQL as PG
import Node.Process (PROCESS)

type ServerConfig =
  { port           :: Int
  , databaseConfig :: PG.PoolConfiguration
  }

databaseConfig :: Config {name :: String} PG.PoolConfiguration
databaseConfig =
  { user: _, password: _, host: _, port: _, database: _, max: _, idleTimeoutMillis: _ }
  <$> string {name: "user"}
  <*> string {name: "password"}
  <*> string {name: "host"}
  <*> int    {name: "port"}
  <*> string {name: "database"}
  <*> int    {name: "pool_size"}
  <*> int    {name: "idle_conn_timeout_millis"}

portConfig :: Config {name :: String} Int
portConfig = int {name: "port"}

serverConfig :: Config {name :: String} ServerConfig
serverConfig =
  { port: _, databaseConfig: _}
  <$> portConfig
  <*> prefix {name: "db"} databaseConfig

readServerConfig :: forall eff.
                    Eff (process :: PROCESS | eff) (Either (Set String) ServerConfig)
readServerConfig = fromEnv "SS" serverConfig

We use the applicative DSL provided in Data.Config module to build a description of our configuration. This description contains the keys and types of the configuration, for consumption by various interpreters. Then we use the fromEnv interpreter to read the config from the environment variables derived from the name fields in the records in the description in the readServerConfig function. We also write a bash script to set those environment variables in the development environment in the setenv.sh file:

export SS_PORT=4000
export SS_DB_USER="abhinav"
export SS_DB_PASSWORD=""
export SS_DB_HOST="localhost"
export SS_DB_PORT=5432
export SS_DB_DATABASE="simple_service"
export SS_DB_POOL_SIZE=10
export SS_DB_IDLE_CONN_TIMEOUT_MILLIS=1000

Now we rewrite our src/Main.purs file to use the readServerConfig function:

module Main where

import Prelude

import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)
import Data.Either (Either(..))
import Data.Set (toUnfoldable)
import Data.String (joinWith)
import Database.PostgreSQL as PG
import Node.Express.Types (EXPRESS)
import Node.Process (PROCESS)
import Node.Process as Process
import SimpleService.Config (readServerConfig)
import SimpleService.Server (runServer)

main :: forall eff. Eff ( console :: CONSOLE
                        , express :: EXPRESS
                        , postgreSQL :: PG.POSTGRESQL
                        , process :: PROCESS
                        | eff ) Unit
main = readServerConfig >>= case _ of
  Left missingKeys -> do
    log $ "Unable to start. Missing Env keys: " <> joinWith ", " (toUnfoldable missingKeys)
    Process.exit 1
  Right { port, databaseConfig } -> runServer port databaseConfig

If readServerConfig fails, we print the missing keys to the console and exit the process. Else we run the server with the read config.

To test this, we stop the server we ran in the beginning, source the config, and run it again:

$ pulp --watch run
* Building project in /Users/abhinav/ps-simple-rest-service
* Build successful.
Server listening on :4000
^C
$ source setenv.sh
$ pulp --watch run
* Building project in /Users/abhinav/ps-simple-rest-service
* Build successful.
Server listening on :4000

It works! We test the failure case by opening another terminal which does not have the environment variables set:

$ pulp run
* Building project in /Users/abhinav/ps-simple-rest-service
* Build successful.
Unable to start. Missing Env keys: SS_DB_DATABASE, SS_DB_HOST, SS_DB_IDLE_CONN_TIMEOUT_MILLIS, SS_DB_PASSWORD, SS_DB_POOL_SIZE, SS_DB_PORT, SS_DB_USER, SS_PORT
* ERROR: Subcommand terminated with exit code 1

Up next, we add logging to our application.

Logging

For logging, we use the purescript-logging package. We write a logger which logs to stdout; in the src/SimpleService/Logger.purs file:

module SimpleService.Logger
  ( debug
  , info
  , warn
  , error
  ) where

import Prelude

import Control.Logger as L
import Control.Monad.Eff.Class (class MonadEff, liftEff)
import Control.Monad.Eff.Console as C
import Control.Monad.Eff.Now (NOW, now)
import Data.DateTime.Instant (toDateTime)
import Data.Either (fromRight)
import Data.Formatter.DateTime (Formatter, format, parseFormatString)
import Data.Generic.Rep (class Generic)
import Data.Generic.Rep.Show (genericShow)
import Data.String (toUpper)
import Partial.Unsafe (unsafePartial)

data Level = Debug | Info | Warn | Error

derive instance eqLevel :: Eq Level
derive instance ordLevel :: Ord Level
derive instance genericLevel :: Generic Level _

instance showLevel :: Show Level where
  show = toUpper <<< genericShow

type Entry =
  { level   :: Level
  , message :: String
  }

dtFormatter :: Formatter
dtFormatter = unsafePartial $ fromRight $ parseFormatString "YYYY-MM-DD HH:mm:ss.SSS"

logger :: forall m e. (
          MonadEff (console :: C.CONSOLE, now :: NOW | e) m) => L.Logger m Entry
logger = L.Logger $ \{ level, message } -> liftEff do
  time <- toDateTime <$> now
  C.log $ "[" <> format dtFormatter time <> "] " <> show level <> " " <> message

log :: forall m e.
        MonadEff (console :: C.CONSOLE , now :: NOW | e) m
     => Entry -> m Unit
log entry@{level} = L.log (L.cfilter (\e -> e.level == level) logger) entry

debug :: forall m e.
         MonadEff (console :: C.CONSOLE , now :: NOW | e) m => String -> m Unit
debug message = log { level: Debug, message }

info :: forall m e.
        MonadEff (console :: C.CONSOLE , now :: NOW | e) m => String -> m Unit
info message = log { level: Info, message }

warn :: forall m e.
        MonadEff (console :: C.CONSOLE , now :: NOW | e) m => String -> m Unit
warn message = log { level: Warn, message }

error :: forall m e.
         MonadEff (console :: C.CONSOLE , now :: NOW | e) m => String -> m Unit
error message = log { level: Error, message }

purescript-logging lets us define our own logging levels and loggers. We define four log levels, and a log entry type with the log level and the message. Then we write the logger which will print the log entry to stdout along with the current time as a well formatted string. We define convenience functions for each log level.

Before we proceed, let’s install the required dependencies.

$ bower install --save purescript-logging purescript-now purescript-formatters

Now we add a request logger middleware to our server in the src/SimpleService/Server.purs file:

-- previous code
import Control.Monad.Eff.Console (CONSOLE)
import Control.Monad.Eff.Now (NOW)
import Data.Maybe (maybe)
import Data.String (toUpper)
import Node.Express.App (App, all, delete, get, http, listenHttp, post, use, useExternal, useOnError)
import Node.Express.Handler (Handler, next)
import Node.Express.Request (getMethod, getPath)
import SimpleService.Logger as Log
-- previous code

requestLogger :: forall eff. Handler (console :: CONSOLE, now :: NOW | eff)
requestLogger = do
  method <- getMethod
  path   <- getPath
  Log.debug $ "HTTP: " <> maybe "" id ((toUpper <<< show) <$> method) <> " " <> path
  next

app :: forall eff.
       PG.Pool
    -> App (postgreSQL :: PG.POSTGRESQL, console :: CONSOLE, now :: NOW | eff)
app pool = do
  useExternal jsonBodyParser
  use requestLogger
  -- previous code

We also convert all our previous logging statements which used Console.log to use SimpleService.Logger and add logs in our handlers. We can see logging in effect by restarting the server and hitting it:

$ pulp --watch run
* Building project in /Users/abhinav/ps-simple-rest-service
* Build successful.
[2017-09-30 16:02:41.634] INFO Server listening on :4000
[2017-09-30 16:02:43.494] DEBUG HTTP: PATCH /v1/user/3
[2017-09-30 16:02:43.517] DEBUG Updated user: 3
[2017-09-30 16:03:46.615] DEBUG HTTP: DELETE /v1/user/3
[2017-09-30 16:03:46.635] DEBUG Deleted user 3
[2017-09-30 16:05:03.805] DEBUG HTTP: GET /v1/users

Conclusion

In this tutorial we learned how to create a simple JSON REST web service written in PureScript with persistence, validation, configuration and logging. The complete code for this tutorial can be found in github. This post can be discussed on r/purescript.

If you liked this post, please leave a comment.

Original post by Abhinav Sarkar - check out All posts on abhinavsarkar.net

Writing a Simple REST Web Service in PureScript - Part 1

23 minute read

At Nilenso, we’ve been working with a client who has chosen PureScript as their primary programming language. Since I couldn’t find any canonical documentation on writing a web service in PureScript, I thought I’d jot down the approach that we took.

The aim of this two-part tutorial is to create a simple JSON REST web service written in PureScript, to run on a node.js server. This assumes that you have basic proficiency with PureScript. We have the following requirements:

  1. persisting users into a Postgres database.
  2. API endpoints for creating, updating, getting, listing and deleting users.
  3. validation of API requests.
  4. reading the server and database configs from environment variables.
  5. logging HTTP requests and debugging info.

In this part we’ll work on setting up the project and on the first two requirements. In the next part we’ll work on the rest of the requirements.

Setting Up

We start with installing PureScript and the required tools. This assumes that we have node and npm installed on our machine.

$ mkdir -p ~/.local/
$ npm install -g purescript pulp bower --prefix ~/.local/

Pulp is a build tool for PureScript projects and bower is a package manager used to get PureScript libraries. We’ll have to add ~/.local/bin in our $PATH (if it is not already added) to access the binaries installed.

Let’s create a directory for our project and make Pulp initialize it:

$ mkdir ps-simple-rest-service
$ cd ps-simple-rest-service
$ pulp init
$ ls
bower.json  bower_components  src  test
$ cat bower.json
{
  "name": "ps-simple-rest-service",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "output"
  ],
  "dependencies": {
    "purescript-prelude": "^3.1.0",
    "purescript-console": "^3.0.0"
  },
  "devDependencies": {
    "purescript-psci-support": "^3.0.0"
  }
}
$ ls bower_components
purescript-console  purescript-eff  purescript-prelude purescript-psci-support

Pulp creates the basic project structure for us. src directory will contain the source while the test directory will contain the tests. bower.json contains the PureScript libraries as dependencies which are downloaded and installed in the bower_components directory.

Types First

First, we create the types needed in src/SimpleService/Types.purs:

module SimpleService.Types where

import Prelude

import Data.Foreign.Class (class Decode, class Encode)
import Data.Foreign.Generic (defaultOptions, genericDecode, genericEncode)
import Data.Generic.Rep (class Generic)
import Data.Generic.Rep.Show (genericShow)

type UserID = Int

newtype User = User
  { id   :: UserID
  , name :: String
  }

derive instance genericUser :: Generic User _

instance showUser :: Show User where
  show = genericShow

instance decodeUser :: Decode User where
  decode = genericDecode $ defaultOptions { unwrapSingleConstructors = true }

instance encodeUser :: Encode User where
  encode = genericEncode $ defaultOptions { unwrapSingleConstructors = true }

We are using the generic support for PureScript types from the purescript-generics-rep and purescript-foreign-generic libraries to encode and decode the User type to JSON. We install the library by running the following command:

$ bower install purescript-foreign-generic --save

Now we can load up the module in the PureScript REPL and try out the JSON conversion features:

$ pulp repl
> import SimpleService.Types
> user = User { id: 1, name: "Abhinav"}
> user
(User { id: 1, name: "Abhinav" })

> import Data.Foreign.Generic
> userJSON = encodeJSON user
> userJSON
"{\"name\":\"Abhinav\",\"id\":1}"

> import Data.Foreign
> import Control.Monad.Except.Trans
> import Data.Identity
> dUser = decodeJSON userJSON :: F User
> eUser = let (Identity eUser) = runExceptT $ dUser in eUser
> eUser
(Right (User { id: 1, name: "Abhinav" }))

We use encodeJSON and decodeJSON functions from the Data.Foreign.Generic module to encode and decode the User instance to JSON. The return type of decodeJSON is a bit complicated as it needs to return the parsing errors too. In this case, the decoding returns no errors and we get back a Right with the correctly parsed User instance.

Persisting It

Next, we add the support for saving a User instance to a Postgres DB. First, we install the required libraries using bower and npm: pg for Javascript bindings to call Postgres, purescript-aff for asynchronous processing and purescript-postgresql-client for PureScript wrapper over pg:

$ npm init -y
$ npm install pg@6.4.0 --save
$ bower install purescript-aff --save
$ bower install purescript-postgresql-client --save

Before writing the code, we create the database and the users table using the command-line Postgres client:

$ psql postgres
psql (9.5.4)
Type "help" for help.

postgres=# create database simple_service;
CREATE DATABASE
postgres=# \c simple_service
You are now connected to database "simple_service" as user "abhinav".
simple_service=# create table users (id int primary key, name varchar(100) not null);
CREATE TABLE
simple_service=# \d users
            Table "public.users"
 Column |          Type          | Modifiers
--------+------------------------+-----------
 id     | integer                | not null
 name   | character varying(100) | not null
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)

Now we add support for converting a User instance to-and-from an SQL row by adding the following code in the src/SimpleService/Types.purs file:

import Data.Array as Array
import Data.Either (Either(..))
import Database.PostgreSQL (class FromSQLRow, class ToSQLRow, fromSQLValue, toSQLValue)

-- code written earlier

instance userFromSQLRow :: FromSQLRow User where
  fromSQLRow [id, name] =
    User <$> ({ id: _, name: _} <$> fromSQLValue id <*> fromSQLValue name)

  fromSQLRow xs = Left $ "Row has " <> show n <> " fields, expecting 2."
    where n = Array.length xs

instance userToSQLRow :: ToSQLRow User where
  toSQLRow (User {id, name}) = [toSQLValue id, toSQLValue name]

We can try out the persistence support in the REPL:

$ pulp repl
PSCi, version 0.11.6
Type :? for help

import Prelude
>
> import SimpleService.Types
> import Control.Monad.Aff (launchAff, liftEff')
> import Database.PostgreSQL as PG
> user = User { id: 1, name: "Abhinav" }
> databaseConfig = {user: "abhinav", password: "", host: "localhost", port: 5432, database: "simple_service", max: 10, idleTimeoutMillis: 1000}

> :paste
… void $ launchAff do
…   pool <- PG.newPool databaseConfig
…   PG.withConnection pool $ \conn -> do
…     PG.execute conn (PG.Query "insert into users (id, name) values ($1, $2)") user
…
unit

> import Data.Foldable (for_)
> import Control.Monad.Eff.Console (logShow)
> :paste
… void $ launchAff do
…   pool <- PG.newPool databaseConfig
…   PG.withConnection pool $ \conn -> do     users :: Array User <- PG.query conn (PG.Query "select id, name from users where id = $1") (PG.Row1 1)
…     liftEff' $ void $ for_ users logShow
…
unit
(User { id: 1, name: "Abhinav" })

We create the databaseConfig record with the configs needed to connect to the database. Using the recond, we create a new Postgres connection pool (PG.newPool) and get a connection from it (PG.withConnection). We call PG.execute with the connection, the SQL insert query for the users table and the User instance, to insert the user into the table. All of this is done inside launchAff which takes care of sequencing the callbacks correctly to make the asynchronous code look synchronous.

Similarly, in the second part, we query the table using PG.query function by calling it with a connection, the SQL select query and the User ID as the query parameter. It returns an Array of users which we log to the console using the logShow function.

We use this experiment to write the persistence related code in the src/SimpleService/Persistence.purs file:

module SimpleService.Persistence
  ( insertUser
  , findUser
  , updateUser
  , deleteUser
  , listUsers
  ) where

import Prelude

import Control.Monad.Aff (Aff)
import Data.Array as Array
import Data.Maybe (Maybe)
import Database.PostgreSQL as PG
import SimpleService.Types (User(..), UserID)

insertUserQuery :: String
insertUserQuery = "insert into users (id, name) values ($1, $2)"

findUserQuery :: String
findUserQuery = "select id, name from users where id = $1"

updateUserQuery :: String
updateUserQuery = "update users set name = $1 where id = $2"

deleteUserQuery :: String
deleteUserQuery = "delete from users where id = $1"

listUsersQuery :: String
listUsersQuery = "select id, name from users"

insertUser :: forall eff. PG.Connection -> User
           -> Aff (postgreSQL :: PG.POSTGRESQL | eff) Unit
insertUser conn user =
  PG.execute conn (PG.Query insertUserQuery) user

findUser :: forall eff. PG.Connection -> UserID
         -> Aff (postgreSQL :: PG.POSTGRESQL | eff) (Maybe User)
findUser conn userID =
  map Array.head $ PG.query conn (PG.Query findUserQuery) (PG.Row1 userID)

updateUser :: forall eff. PG.Connection -> User
           -> Aff (postgreSQL :: PG.POSTGRESQL | eff) Unit
updateUser conn (User {id, name}) =
  PG.execute conn (PG.Query updateUserQuery) (PG.Row2 name id)

deleteUser :: forall eff. PG.Connection -> UserID
           -> Aff (postgreSQL :: PG.POSTGRESQL | eff) Unit
deleteUser conn userID =
  PG.execute conn (PG.Query deleteUserQuery) (PG.Row1 userID)

listUsers :: forall eff. PG.Connection
          -> Aff (postgreSQL :: PG.POSTGRESQL | eff) (Array User)
listUsers conn =
  PG.query conn (PG.Query listUsersQuery) PG.Row0

Serving It

We can now write a simple HTTP API over the persistence layer using Express to provide CRUD functionality for users. Let’s install Express and purescript-express, the PureScript wrapper over it:

$ npm install express --save
$ bower install purescript-express --save

Getting a User

We do this top-down. First, we change src/Main.purs to run the HTTP server by providing the server port and database configuration:

module Main where

import Prelude

import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE)
import Database.PostgreSQL as PG
import Node.Express.Types (EXPRESS)
import SimpleService.Server (runServer)

main :: forall eff. Eff ( console :: CONSOLE
                        , express :: EXPRESS
                        , postgreSQL :: PG.POSTGRESQL
                        | eff) Unit
main = runServer port databaseConfig
  where
    port = 4000
    databaseConfig = { user: "abhinav"
                     , password: ""
                     , host: "localhost"
                     , port: 5432
                     , database: "simple_service"
                     , max: 10
                     , idleTimeoutMillis: 1000
                     }

Next, we wire up the server routes to the handlers in src/SimpleService/Server.purs:

module SimpleService.Server (runServer) where

import Prelude

import Control.Monad.Aff (runAff)
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Class (liftEff)
import Control.Monad.Eff.Console (CONSOLE, log, logShow)
import Database.PostgreSQL as PG
import Node.Express.App (App, get, listenHttp)
import Node.Express.Types (EXPRESS)
import SimpleService.Handler (getUser)

app :: forall eff. PG.Pool -> App (postgreSQL :: PG.POSTGRESQL | eff)
app pool = do
  get "/v1/user/:id" $ getUser pool

runServer :: forall eff.
             Int
          -> PG.PoolConfiguration
          -> Eff ( express :: EXPRESS
                 , postgreSQL :: PG.POSTGRESQL
                 , console :: CONSOLE
                 | eff ) Unit
runServer port databaseConfig =  void $ runAff logShow pure do
  pool <- PG.newPool databaseConfig
  let app' = app pool
  void $ liftEff $ listenHttp app' port \_ -> log $ "Server listening on :" <> show port

runServer creates a PostgreSQL connection pool and passes it to the app function which creates the Express application, which in turn, binds it to the handler getUser. Then it launches the HTTP server by calling listenHttp.

Finally, we write the actual getUser handler in src/SimpleService/Handler.purs:

module SimpleService.Handler where

import Prelude

import Control.Monad.Aff.Class (liftAff)
import Data.Foreign.Class (encode)
import Data.Int (fromString)
import Data.Maybe (Maybe(..))
import Database.PostgreSQL as PG
import Node.Express.Handler (Handler)
import Node.Express.Request (getRouteParam)
import Node.Express.Response (end, sendJson, setStatus)
import SimpleService.Persistence as P

getUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
getUser pool = getRouteParam "id" >>= case _ of
  Nothing -> respond 422 { error: "User ID is required" }
  Just sUserId -> case fromString sUserId of
    Nothing -> respond 422 { error: "User ID must be an integer: " <> sUserId }
    Just userId -> liftAff (PG.withConnection pool $ flip P.findUser userId) >>= case _ of
      Nothing -> respond 404 { error: "User not found with id: " <> sUserId }
      Just user -> respond 200 (encode user)

respond :: forall eff a. Int -> a -> Handler eff
respond status body = do
  setStatus status
  sendJson body

respondNoContent :: forall eff. Int -> Handler eff
respondNoContent status = do
  setStatus status
  end

getUser validates the route parameter for valid user ID, sending error HTTP responses in case of failures. It then calls findUser to find the user and returns appropriate response.

We can test this on the command-line using HTTPie. We run pulp --watch run in one terminal to start the server with file watching, and test it from another terminal:

$ pulp --watch run
* Building project in ps-simple-rest-service
* Build successful.
Server listening on :4000
$ http GET http://localhost:4000/v1/user/1 # should return the user we created earlier
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 25
Content-Type: application/json; charset=utf-8
Date: Sun, 10 Sep 2017 14:32:52 GMT
ETag: W/"19-qmtK9XY+WDrqHTgqtFlV+h+NGOY"
X-Powered-By: Express

{
    "id": 1,
    "name": "Abhinav"
}
$ http GET http://localhost:4000/v1/user/s
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 38
Content-Type: application/json; charset=utf-8
Date: Sun, 10 Sep 2017 14:36:04 GMT
ETag: W/"26-//tvORl1gGDUMwgSaqbEpJhuadI"
X-Powered-By: Express

{
    "error": "User ID must be an integer: s"
}
$ http GET http://localhost:4000/v1/user/2
HTTP/1.1 404 Not Found
Connection: keep-alive
Content-Length: 36
Content-Type: application/json; charset=utf-8
Date: Sun, 10 Sep 2017 14:36:11 GMT
ETag: W/"24-IyD5VT4E8/m3kvpwycRBQunI7Uc"
X-Powered-By: Express

{
    "error": "User not found with id: 2"
}

Deleting a User

deleteUser handler is similar. We add the route in the app function in the src/SimpleService/Server.purs file:

-- previous code
import Node.Express.App (App, delete, get, listenHttp)
import SimpleService.Handler (deleteUser, getUser)
-- previous code

app :: forall eff. PG.Pool -> App (postgreSQL :: PG.POSTGRESQL | eff)
app pool = do
  get "/v1/user/:id" $ getUser pool
  delete "/v1/user/:id" $ deleteUser pool

-- previous code

And we add the handler in the src/SimpleService/Handler.purs file:

deleteUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
deleteUser pool = getRouteParam "id" >>= case _ of
  Nothing -> respond 422 { error: "User ID is required" }
  Just sUserId -> case fromString sUserId of
    Nothing -> respond 422 { error: "User ID must be an integer: " <> sUserId }
    Just userId -> do
      found <- liftAff $ PG.withConnection pool \conn -> PG.withTransaction conn do
        P.findUser conn userId >>= case _ of
          Nothing -> pure false
          Just _  -> do
            P.deleteUser conn userId
            pure true
      if found
        then respondNoContent 204
        else respond 404 { error: "User not found with id: " <> sUserId }

After the usual validations on the route param, deleteUser tries to find the user by the given user ID and if found, it deletes the user. Both the persistence related functions are run inside a single SQL transaction using PG.withTransaction function. deleteUser return 404 status if the user is not found, else it returns 204 status.

Let’s try it out:

$ http GET http://localhost:4000/v1/user/1
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 25
Content-Type: application/json; charset=utf-8
Date: Mon, 11 Sep 2017 05:10:50 GMT
ETag: W/"19-GC9FAtbd81t7CtrQgsNuc8HITXU"
X-Powered-By: Express

{
    "id": 1,
    "name": "Abhinav"
}
$ http DELETE http://localhost:4000/v1/user/1
HTTP/1.1 204 No Content
Connection: keep-alive
Date: Mon, 11 Sep 2017 05:10:56 GMT
X-Powered-By: Express
$ http GET http://localhost:4000/v1/user/1
HTTP/1.1 404 Not Found
Connection: keep-alive
Content-Length: 37
Content-Type: application/json; charset=utf-8
Date: Mon, 11 Sep 2017 05:11:03 GMT
ETag: W/"25-Eoc4ZbEF73CyW8EGh6t2jqI8mLU"
X-Powered-By: Express

{
    "error": "User not found with id: 1"
}
$ http DELETE http://localhost:4000/v1/user/1
HTTP/1.1 404 Not Found
Connection: keep-alive
Content-Length: 37
Content-Type: application/json; charset=utf-8
Date: Mon, 11 Sep 2017 05:11:05 GMT
ETag: W/"25-Eoc4ZbEF73CyW8EGh6t2jqI8mLU"
X-Powered-By: Express

{
    "error": "User not found with id: 1"
}

Creating a User

createUser handler is a bit more involved. First, we add an Express middleware to parse the body of the request as JSON. We use body-parser for this and access it through PureScript FFI. We create a new file src/SimpleService/Middleware/BodyParser.js with the content:

"use strict";

var bodyParser = require("body-parser");

exports.jsonBodyParser = bodyParser.json({
  limit: "5mb"
});

And write a wrapper for it in the file src/SimpleService/Middleware/BodyParser.purs with the content:

module SimpleService.Middleware.BodyParser where

import Prelude
import Data.Function.Uncurried (Fn3)
import Node.Express.Types (ExpressM, Response, Request)

foreign import jsonBodyParser ::
  forall e. Fn3 Request Response (ExpressM e Unit) (ExpressM e Unit)

We also install the body-parser npm dependency:

$ npm install --save body-parser

Next, we change the app function in the src/SimpleService/Server.purs file to add the middleware and the route:

-- previous code
import Node.Express.App (App, delete, get, listenHttp, post, useExternal)
import SimpleService.Handler (createUser, deleteUser, getUser)
import SimpleService.Middleware.BodyParser (jsonBodyParser)
-- previous code

app :: forall eff. PG.Pool -> App (postgreSQL :: PG.POSTGRESQL | eff)
app pool = do
  useExternal jsonBodyParser

  get "/v1/user/:id" $ getUser pool
  delete "/v1/user/:id" $ deleteUser pool
  post "/v1/users" $ createUser pool

And finally, we write the handler in the src/SimpleService/Handler.purs file:

-- previous code
import Data.Either (Either(..))
import Data.Foldable (intercalate)
import Data.Foreign (renderForeignError)
import Node.Express.Request (getBody, getRouteParam)
import SimpleService.Types
-- previous code

createUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
createUser pool = getBody >>= case _ of
  Left errs -> respond 422 { error: intercalate ", " $ map renderForeignError errs}
  Right u@(User user) ->
    if user.id <= 0
      then respond 422 { error: "User ID must be positive: " <> show user.id}
      else if user.name == ""
        then respond 422 { error: "User name must not be empty" }
        else do
          liftAff (PG.withConnection pool $ flip P.insertUser u)
          respondNoContent 201

createUser calls getBody which has type signature forall e a. (Decode a) => HandlerM (express :: EXPRESS | e) (Either MultipleErrors a). It returns either a list of parsing errors or a parsed instance, which in our case, is a User. In case of errors, we just return the errors rendered as string with a 422 status. If we get a parsed User instance, we do some validations on it, returning appropriate error messages. If all validations pass, we create the user in the DB by calling insertUser from the persistence layer and respond with a status 201.

We can try it out:

$ http POST http://localhost:4000/v1/users name="abhinav"
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 97
Content-Type: application/json; charset=utf-8
Date: Mon, 11 Sep 2017 05:51:28 GMT
ETag: W/"61-BgsrMukZpImcdwAJEKCZ+70WBb8"
X-Powered-By: Express

{
    "error": "Error at array index 0: (ErrorAtProperty \"id\" (TypeMismatch \"Int\" \"Undefined\"))"
}
$ http POST http://localhost:4000/v1/users id:=1 name=""
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 39
Content-Type: application/json; charset=utf-8
Date: Mon, 11 Sep 2017 05:51:42 GMT
ETag: W/"27-JQsh12xu/rEFdWy8REF4NMtBUB4"
X-Powered-By: Express

{
    "error": "User name must not be empty"
}
$ http POST http://localhost:4000/v1/users id:=1 name="abhinav"
HTTP/1.1 201 Created
Connection: keep-alive
Content-Length: 0
Date: Mon, 11 Sep 2017 05:52:23 GMT
X-Powered-By: Express
$ http GET http://localhost:4000/v1/user/1
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 25
Content-Type: application/json; charset=utf-8
Date: Mon, 11 Sep 2017 05:52:30 GMT
ETag: W/"19-GC9FAtbd81t7CtrQgsNuc8HITXU"
X-Powered-By: Express

{
    "id": 1,
    "name": "abhinav"
}

First try returns a parsing failure because we didn’t provide the id field. Second try is a validation failure because the name was empty. Third try is a success which we check by doing a GET request next.

Updating a User

We want to allow a user’s name to be updated through the API, but not the user’s id. So we add a new type to src/SimpleService/Types.purs to represent a possible change in user’s name:

-- previous code
import Data.Foreign.NullOrUndefined (NullOrUndefined)
-- previous code

newtype UserPatch = UserPatch { name :: NullOrUndefined String }

derive instance genericUserPatch :: Generic UserPatch _

instance decodeUserPatch :: Decode UserPatch where
  decode = genericDecode $ defaultOptions { unwrapSingleConstructors = true }

NullOrUndefined is a wrapper over Maybe with added support for Javascript null and undefined values. We define UserPatch as having a possibly null (or undefined) name field.

Now we can add the corresponding handler in src/SimpleService/Handlers.purs:

-- previous code
import Data.Foreign.NullOrUndefined (unNullOrUndefined)
-- previous code

updateUser :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
updateUser pool = getRouteParam "id" >>= case _ of
  Nothing -> respond 422 { error: "User ID is required" }
  Just sUserId -> case fromString sUserId of
    Nothing -> respond 422 { error: "User ID must be positive: " <> sUserId }
    Just userId -> getBody >>= case _ of
      Left errs -> respond 422 { error: intercalate ", " $ map renderForeignError errs}
      Right (UserPatch userPatch) -> case unNullOrUndefined userPatch.name of
        Nothing -> respondNoContent 204
        Just userName -> if userName == ""
          then respond 422 { error: "User name must not be empty" }
          else do
            savedUser <- liftAff $ PG.withConnection pool \conn -> PG.withTransaction conn do
              P.findUser conn userId >>= case _ of
                Nothing -> pure Nothing
                Just (User user) -> do
                  let user' = User (user { name = userName })
                  P.updateUser conn user'
                  pure $ Just user'
            case savedUser of
              Nothing -> respond 404 { error: "User not found with id: " <> sUserId }
              Just user -> respond 200 (encode user)

After checking for a valid user ID as before, we get the decoded request body as a UserPatch instance. If the path does not have the name field or has it as null, there is nothing to do and we respond with a 204 status. If the user name is present in the patch, we validate it for non-emptiness. Then, within a DB transaction, we try to find the user with the given ID, responding with a 404 status if the user is not found. If the user is found, we update the user’s name in the database, and respond with a 200 status and the saved user encoded as the JSON response body.

Finally, we can add the route to our server’s router in src/SimpleService/Server.purs to make the functionality available:

-- previous code
import Node.Express.App (App, delete, get, http, listenHttp, post, useExternal)
import Node.Express.Types (EXPRESS, Method(..))
import SimpleService.Handler (createUser, deleteUser, getUser, updateUser)
-- previous code

app :: forall eff. PG.Pool -> App (postgreSQL :: PG.POSTGRESQL | eff)
app pool = do
  useExternal jsonBodyParser

  get "/v1/user/:id"    $ getUser pool
  delete "/v1/user/:id" $ deleteUser pool
  post "/v1/users"      $ createUser pool
  patch "/v1/user/:id"  $ updateUser pool
  where
    patch = http (CustomMethod "patch")

We can try it out now:

$ http GET http://localhost:4000/v1/user/1
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 26
Content-Type: application/json; charset=utf-8
Date: Fri, 11 Sep 2017 06:41:10 GMT
ETag: W/"1a-hoLBx55zeY8nZFWJh/kM05pXwSA"
X-Powered-By: Express

{
    "id": 1,
    "name": "abhinav"
}
$ http PATCH http://localhost:4000/v1/user/1 name=abhinavsarkar
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 31
Content-Type: application/json; charset=utf-8
Date: Fri, 11 Sep 2017 06:41:36 GMT
ETag: W/"1f-EG5i0hq/hYhF0BsuheD9hNXeBpI"
X-Powered-By: Express

{
    "id": 1,
    "name": "abhinavsarkar"
}
$ http GET http://localhost:4000/v1/user/1
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 31
Content-Type: application/json; charset=utf-8
Date: Fri, 11 Sep 2017 06:41:40 GMT
ETag: W/"1f-EG5i0hq/hYhF0BsuheD9hNXeBpI"
X-Powered-By: Express

{
    "id": 1,
    "name": "abhinavsarkar"
}
$ http PATCH http://localhost:4000/v1/user/1
HTTP/1.1 204 No Content
Connection: keep-alive
Date: Fri, 11 Sep 2017 06:42:31 GMT
X-Powered-By: Express
$ http PATCH http://localhost:4000/v1/user/1 name=""
HTTP/1.1 422 Unprocessable Entity
Connection: keep-alive
Content-Length: 39
Content-Type: application/json; charset=utf-8
Date: Fri, 11 Sep 2017 06:43:17 GMT
ETag: W/"27-JQsh12xu/rEFdWy8REF4NMtBUB4"
X-Powered-By: Express

{
    "error": "User name must not be empty"
}

Listing all Users

Listing all users is quite simple since it doesn’t require us to take any request parameter.

We add the handler to the src/SimpleService/Handler.purs file:

-- previous code
listUsers :: forall eff. PG.Pool -> Handler (postgreSQL :: PG.POSTGRESQL | eff)
listUsers pool = liftAff (PG.withConnection pool P.listUsers) >>= encode >>> respond 200

And the route to the src/SimpleService/Server.purs file:

-- previous code
import SimpleService.Handler (createUser, deleteUser, getUser, listUsers, updateUser)
-- previous code

app :: forall eff. PG.Pool -> App (postgreSQL :: PG.POSTGRESQL | eff)
app pool = do
  useExternal jsonBodyParser

  get "/v1/user/:id"    $ getUser pool
  delete "/v1/user/:id" $ deleteUser pool
  post "/v1/users"      $ createUser pool
  patch "/v1/user/:id"  $ updateUser pool
  get "/v1/users"       $ listUsers pool
  where
    patch = http (CustomMethod "patch")

And that’s it. We can test this endpoint:

$ http POST http://localhost:4000/v1/users id:=2 name=sarkarabhinav
HTTP/1.1 201 Created
Connection: keep-alive
Content-Length: 0
Date: Fri, 11 Sep 2017 07:06:24 GMT
X-Powered-By: Express
$ http GET http://localhost:4000/v1/users
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 65
Content-Type: application/json; charset=utf-8
Date: Fri, 11 Sep 2017 07:06:27 GMT
ETag: W/"41-btt9uNdG+9A1RO7SCLOsyMmIyFo"
X-Powered-By: Express

[
    {
        "id": 1,
        "name": "abhinavsarkar"
    },
    {
        "id": 2,
        "name": "sarkarabhinav"
    }
]

Conclusion

That concludes the first part of the two-part tutorial. We learned how to set up a PureScript project, how to access a Postgres database and how to create a JSON REST API over the database. The code till the end of this part can be found in github. In the next part, we’ll learn how to do API validation, application configuration and logging. This post can be discussed on r/purescript.

If you liked this post, please leave a comment.

Original post by Abhinav Sarkar - check out All posts on abhinavsarkar.net

Gunak — Designing with the Indian Government

Working with a government body to digitise their offline hospital quality assurance system

What’s the problem?

The NRHM (National Rural Health Mission) is an initiative undertaken by the Government of India to address the health needs of under-served rural areas. The NRHM was initially tasked with addressing the health needs of 18 states that had been identified as having weak public health indicators.

In an effort to better this setup, the Government of India setup the NHSRC (National Health Systems Resource Centre), under the NRHM, to serve as an apex body for technical assistance. The goal of this institution is to improve health outcomes by facilitating governance reform, health systems innovations, and improved information sharing among all stakeholders — at the national, state, district and sub-district levels — through capacity development and convergence models.

One of the areas that the NHSRC works in is the provision of universal healthcare services. Universal access to good quality services — services that are effective, that are safe and satisfying to the patient; services that are patient and community centred, and services that make efficient use of the limited resources available.

The approach for achieving these objectives involves ensuring that every single health facility is scored against pre-defined standards with periodic supportive supervision for ensuring continual improvement.

There are three main components to conducting an assessment,

- Facilities
- Assessments themselves, and
- Facilitators

Facilitators conduct Assessments for Facilities.

Facilities are hospitals or other healthcare systems that exist at different levels,

  • National level
  • State level
  • District level
  • District Hospital level

Based on their level, facilities would be eligible for a particular type of assessment.

There are two main types of assessments in the system,

  • National Quality Assessment System (NQAS): Is a system which has incorporated best practices from the contemporary systems, and contextualised them for meeting the needs of Public Health System in the country
  • Kayakalp assessment: To complement the government’s ‘Swachh Bharat Abhiyan’ (cleanliness in public spaces campaign), the Ministry of Health & Family welfare, Government of India launched a National Initiative to present awards to public health facilities that demonstrate high levels of cleanliness, hygiene and infection control

Assessments are a set of pre-defined standards against which facilities are scored. Assessments happen periodically throughout the year under supervision to ensure continual improvement.

Facilitators are people with varied experience, who are picked based on the type of assessment. For instance, National level assessments would require facilitators with a certain set of requirements as compared to a State level assessments. For instance, national level facilitators will consist of representatives from the programme divisions (maternal health, child health, family planning, etc.) of the Ministry of Health and Family Welfare, Government of India and National Health Systems Resource Centre.

How does the assessment process work?

The assessment process requires Facilitators to give a Score (of 0, 1 or 2) to a Measurable Element (ME).

Each ME belongs to an Area of Concern (AoC) and an AoC belongs to a Standard.

Standards: These are broad thematic areas with respect to cleanliness & hygiene, and can be termed as the “pillars” of the system.

Area of Concern: Each theme (Standard), has a fixed number of criteria that cover specific attributes.

Measurable Element: Is the lowest and most tangible unit of an assessment. MEs are specific requirements that the facilitators are expected to look for in a facility for ascertaining the extent of compliance and award a score.

Checkpoints: In the NQAS type assessment, MEs are broken down further into Checkpoints. Checkpoints are departmental checklists.

This is how the above-mentioned elements exist in the Assessments,

Assessments for Facilities are done via Checklists, which encapsulate all of the above (Standards, AoCs, MEs and Checkpoints).

How were assessments conducted prior to the app?

Facilitators were handed sheets of paper with all the Standards, AoC and ME on them. Facilitators would traverse back and forth through this list, assign a compliance score to a ME until they were done with all AoC and subsequently with all Standards.

Advantages of the offline system,

  • Easy and fast scanning of Standards, AoCs and MEs
  • This medium requires little to no training

Disadvantages of the the offline system,

  • Hard to keep track of progress. Facilitators have to scan and do this themselves by repeatedly going through the list
  • Force facilitators to conduct their assessments by the list, rather than what is convenient
  • Unobliging towards writing comments for scores awarded to MEs
  • Can’t generate reports immediately
  • Submitting Assessments as a physical copy and manually syncing the results across the entire district/state/nation

What is Gunak?

Based on the disadvantages mentioned above, Nikhil (senior member of the NHSRC) thought it’d be best to use technology to make the process of conducting and syncing assessments easier. Gunak is the app that aimed at doing this.

Minimum feature list we aimed to launch the app with,

  • Conduct NQAS and Kayakalp assessments
  • Motivate facilitators to enter comments for the scores they give
  • Allow assessments to be conducted offline
  • Sync assessments once facilitators were in an area with active internet connection
  • View detailed reports across all Standards, AoCs and MEs for finished assessments
  • Sync reports of finished assessments for peers/superiors to view/review

The challenges we faced,

  • Maintain familiarity while transitioning facilitators from an offline to an online system
  • Translating a multi-tier architecture to mobile

Navigation

Usage: For an assessment to be complete, facilitators have to finish all MEs across all AoCs.

Usage Pattern: Facilitators, physically visit facilities to conduct assessments. The offline medium sometimes forced the facilitators to finish Measurable Elements in a sequential order (as displayed on paper) since it would be easier to keep track of the progress of the assessment.

Notes:

  1. MEs are specific department-wise questions for a facility. Since most facilities aren’t built the same way, following a sequential pattern wasn’t the way to go ahead.
  2. The assessments are usually carried out over a period of 3–5 days.

We wanted to design a system that would be flexible and not dictate facilitator behaviour. A system that would always keep the facilitator aware of where they are and their progress. In addition to this, we had to ensure that readability and usability were the main focus since facilitators moved around a lot while conducting assessments.

The bottom navigation for an ME, allows facilitators to know the progress of the AoC these MEs belong to. It works as a navigation system where facilitators can jump forward to answer an ME and come back to answer another ME.

Search

We wanted to find an equivalent for the ‘flip & find’ function, that facilitators perform on paper, for the app.

Search allows facilitators to find MEs, AoCs and Standards.

Our Search feature isn’t a simple text match. As seen below, it can identify that the word ‘hygiene’, could pertain to a Standard, an AoC or MEs.

Reports

One of the main drawbacks of the offline medium was its inability to generate reports. A facilitator or their superior would spend hours going through an assessment and assign a score for the same.

We took advantage of technology to provide detailed reports based on Departments, AoC and Score, which can be shared as an excel sheet or as an image.

Who did we work with?

This project would not have been possible if it wasn’t for the amazing team at Samanvay. This is our second project with them, you can read about the first one here,

Designing for Rural India — Part 1

App Launch and Reception

The Gunak app is being rolled out in phases. Its first launch was in August 2017 by the Ministry of Health and Family Welfare, the app has a 4.8* rating on the play store.

Thank you so much for reading this. If you found this interesting, don’t forget to 👏 👏

If you have questions or thoughts about the post and/or would like to reach out to us outside of Medium, send us an email, moshimoshi@nilenso.com

🙏


Gunak — Designing with the Indian Government was originally published in uxdesign.cc on Medium, where people are continuing the conversation by highlighting and responding to this story.

Original post by Varun Pai - check out Stories by Varun Pai on Medium