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.

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.

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:

persisting users into a Postgres database. API endpoints for creating, updating, getting, listing and deleting users. validation of API requests. reading the server and database configs from environment variables. 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/ -p ~/.local/ $ npm install -g purescript pulp bower --prefix ~/.local/ 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 ps-simple-rest-service $ cd ps-simple-rest-service ps-simple-rest-service $ pulp init init $ ls bower.json bower_components src test bower_components src test $ cat bower.json 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 ) (class, class import Data.Foreign.Generic (defaultOptions, genericDecode, genericEncode) (defaultOptions, genericDecode, genericEncode) import Data.Generic.Rep (class Generic ) (class import Data.Generic.Rep.Show (genericShow) (genericShow) type UserID = Int newtype User = User { id :: UserID , name :: String } instance genericUser :: Generic User _ derive instance showUser :: Show User where show = genericShow genericShow instance decodeUser :: Decode User where = genericDecode $ defaultOptions { unwrapSingleConstructors = true } decodegenericDecodedefaultOptions { unwrapSingleConstructorstrue } instance encodeUser :: Encode User where = genericEncode $ defaultOptions { unwrapSingleConstructors = true } encodegenericEncodedefaultOptions { unwrapSingleConstructorstrue }

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 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 pulp repl > import SimpleService.Types > user = User { id : 1 , name : "Abhinav" } user, name > user user ( User { id : 1 , name : "Abhinav" }) , name}) > import Data.Foreign.Generic > userJSON = encodeJSON user userJSONencodeJSON user > userJSON userJSON "{\"name\":\"Abhinav\",\"id\":1}" > import Data.Foreign > import Control.Monad.Except.Trans > import Data.Identity > dUser = decodeJSON userJSON :: F User dUserdecodeJSON > eUser = let ( Identity eUser) = runExceptT $ dUser in eUser eUsereUser)runExceptTdUsereUser > eUser eUser ( Right ( User { id : 1 , name : "Abhinav" })) , name}))

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 database. 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 :

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) (class, class, fromSQLValue, toSQLValue) -- code written earlier instance userFromSQLRow :: FromSQLRow User where id , name] = fromSQLRow [, name] User <$> ({ id : _, name : _} <$> fromSQLValue id <*> fromSQLValue name) ({_, name_}fromSQLValuefromSQLValue name) = Left $ "Row has " <> show n <> " fields, expecting 2." fromSQLRow xs where n = Array.length xs Array.length xs instance userToSQLRow :: ToSQLRow User where User { id , name}) = [toSQLValue id , toSQLValue name] toSQLRow (, name})[toSQLValue, toSQLValue name]

We can try out the persistence support in the REPL:

$ pulp repl pulp repl PSCi , version 0.11 . 6 , version Type :? for help for help import Prelude > > import SimpleService.Types > import Control.Monad.Aff (launchAff, liftEff') (launchAff, liftEff') > import Database.PostgreSQL as PG > user = User { id : 1 , name : "Abhinav" } user, name > databaseConfig = {user : "abhinav" , password : "" , host : "localhost" , port : 5432 , database : "simple_service" , max : 10 , idleTimeoutMillis : 1000 } databaseConfig{user, password, host, port, database, idleTimeoutMillis > : paste paste $ launchAff do … voidlaunchAff <- PG.newPool databaseConfig … poolPG.newPool databaseConfig $ \conn -> do … PG.withConnection pool\conn PG.Query "insert into users (id, name) values ($1, $2)" ) user … PG.execute conn () user … unit > import Data.Foldable (for_) (for_) > import Control.Monad.Eff.Console (logShow) (logShow) > : paste paste $ launchAff do … voidlaunchAff <- PG.newPool databaseConfig … poolPG.newPool databaseConfig $ \conn -> do … PG.withConnection pool\conn … users :: Array User <- PG.query conn ( PG.Query "select id, name from users where id = $1" ) ( PG.Row1 1 ) PG.query conn () ( $ void $ for_ users logShow … liftEff'voidfor_ users logShow … unit ( User { id : 1 , name : "Abhinav" }) , name})

We create the databaseConfig record with the configs needed to connect to the database. Using the record, 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 = "insert into users (id, name) values ($1, $2)" insertUserQuery findUserQuery :: String = "select id, name from users where id = $1" findUserQuery updateUserQuery :: String = "update users set name = $1 where id = $2" updateUserQuery deleteUserQuery :: String = "delete from users where id = $1" deleteUserQuery listUsersQuery :: String = "select id, name from users" listUsersQuery insertUser :: forall eff . PG.Connection -> User eff -> Aff ( postgreSQL :: PG.POSTGRESQL | eff) Unit eff) = insertUser conn user PG.Query insertUserQuery) user PG.execute conn (insertUserQuery) user findUser :: forall eff . PG.Connection -> UserID eff -> Aff ( postgreSQL :: PG.POSTGRESQL | eff) ( Maybe User ) eff) ( = findUser conn userID map Array.head $ PG.query conn ( PG.Query findUserQuery) ( PG.Row1 userID) Array.headPG.query conn (findUserQuery) (userID) updateUser :: forall eff . PG.Connection -> User eff -> Aff ( postgreSQL :: PG.POSTGRESQL | eff) Unit eff) User { id , name}) = updateUser conn (, name}) PG.Query updateUserQuery) ( PG.Row2 name id ) PG.execute conn (updateUserQuery) (name deleteUser :: forall eff . PG.Connection -> UserID eff -> Aff ( postgreSQL :: PG.POSTGRESQL | eff) Unit eff) = deleteUser conn userID PG.Query deleteUserQuery) ( PG.Row1 userID) PG.execute conn (deleteUserQuery) (userID) listUsers :: forall eff . PG.Connection eff -> Aff ( postgreSQL :: PG.POSTGRESQL | eff) ( Array User ) eff) ( = listUsers conn PG.Query listUsersQuery) PG.Row0 PG.query conn (listUsersQuery)

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 install express --save $ bower install purescript-express --save 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) (runServer) main :: forall eff . Eff ( console :: CONSOLE eff , express :: EXPRESS , postgreSQL :: PG.POSTGRESQL | eff) Unit eff) = runServer port databaseConfig mainrunServer port databaseConfig where = 4000 port = { user : "abhinav" databaseConfig{ user : "" , password : "localhost" , host : 5432 , port : "simple_service" , database , max : 10 : 1000 , idleTimeoutMillis }

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

module SimpleService.Server (runServer) where (runServer) import Prelude import Control.Monad.Aff (runAff) (runAff) import Control.Monad.Eff ( Eff ) import Control.Monad.Eff.Class (liftEff) (liftEff) import Control.Monad.Eff.Console ( CONSOLE , log, logShow) , log, logShow) import Database.PostgreSQL as PG import Node.Express.App ( App , get, listenHttp) , get, listenHttp) import Node.Express.Types ( EXPRESS ) import SimpleService.Handler (getUser) (getUser) app :: forall eff . PG.Pool -> App ( postgreSQL :: PG.POSTGRESQL | eff) effeff) = do app pool "/v1/user/:id" $ getUser pool getgetUser pool runServer :: forall eff . eff Int -> PG.PoolConfiguration -> Eff ( express :: EXPRESS , postgreSQL :: PG.POSTGRESQL , console :: CONSOLE | eff ) Unit eff ) = void $ runAff logShow pure do runServer port databaseConfigvoidrunAff logShow <- PG.newPool databaseConfig poolPG.newPool databaseConfig let app' = app pool app'app pool $ liftEff $ listenHttp app' port \_ -> log $ "Server listening on :" <> show port voidliftEfflistenHttp app' port \_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) (liftAff) import Data.Foreign.Class (encode) (encode) import Data.Int (fromString) (fromString) import Data.Maybe ( Maybe (..)) (..)) import Database.PostgreSQL as PG import Node.Express.Handler ( Handler ) import Node.Express.Request (getRouteParam) (getRouteParam) import Node.Express.Response (end, sendJson, setStatus) (end, sendJson, setStatus) import SimpleService.Persistence as P getUser :: forall eff . PG.Pool -> Handler ( postgreSQL :: PG.POSTGRESQL | eff) effeff) = getRouteParam "id" >>= case _ of getUser poolgetRouteParam Nothing -> respond 422 { error : "User ID is required" } respond Just sUserId -> case fromString sUserId of sUserIdfromString sUserId Nothing -> respond 422 { error : "User ID must be an integer: " <> sUserId } respondsUserId } Just userId -> liftAff (PG.withConnection pool $ flip P.findUser userId) >>= case _ of userIdliftAff (PG.withConnection poolP.findUser userId) Nothing -> respond 404 { error : "User not found with id: " <> sUserId } respondsUserId } Just user -> respond 200 (encode user) userrespond(encode user) respond :: forall eff a . Int -> a -> Handler eff eff aeff = do respond status body setStatus status sendJson body respondNoContent :: forall eff . Int -> Handler eff effeff = do respondNoContent status 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 --watch run * Building project in ps-simple-rest-service Building project in ps-simple-rest-service * Build successful. Build successful. Server listening on :4000 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) , delete, get, listenHttp) import SimpleService.Handler (deleteUser, getUser) (deleteUser, getUser) -- previous code app :: forall eff . PG.Pool -> App ( postgreSQL :: PG.POSTGRESQL | eff) effeff) = do app pool "/v1/user/:id" $ getUser pool getgetUser pool "/v1/user/:id" $ deleteUser pool deletedeleteUser 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) effeff) = getRouteParam "id" >>= case _ of deleteUser poolgetRouteParam Nothing -> respond 422 { error : "User ID is required" } respond Just sUserId -> case fromString sUserId of sUserIdfromString sUserId Nothing -> respond 422 { error : "User ID must be an integer: " <> sUserId } respondsUserId } Just userId -> do userId <- liftAff $ PG.withConnection pool \conn -> PG.withTransaction conn do foundliftAffPG.withConnection pool \connPG.withTransaction conn >>= case _ of P.findUser conn userId Nothing -> pure false false Just _ -> do P.deleteUser conn userId pure true true if found found then respondNoContent 204 respondNoContent else respond 404 { error : "User not found with id: " <> sUserId } respondsUserId }

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" ) ; bodyParser . jsonBodyParser = bodyParser . json ({ exportsbodyParser({ 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 ) import jsonBodyParser :: foreignjsonBodyParser :: forall e . Fn3 Request Response ( ExpressM e Unit ) ( ExpressM e Unit ) ) (

We also install the body-parser npm dependency:

$ npm install --save body-parser 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) , delete, get, listenHttp, post, useExternal) import SimpleService.Handler (createUser, deleteUser, getUser) (createUser, deleteUser, getUser) import SimpleService.Middleware.BodyParser (jsonBodyParser) (jsonBodyParser) -- previous code app :: forall eff . PG.Pool -> App ( postgreSQL :: PG.POSTGRESQL | eff) effeff) = do app pool useExternal jsonBodyParser "/v1/user/:id" $ getUser pool getgetUser pool "/v1/user/:id" $ deleteUser pool deletedeleteUser pool "/v1/users" $ createUser pool postcreateUser pool

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

-- previous code import Data.Either ( Either (..)) (..)) import Data.Foldable (intercalate) (intercalate) import Data.Foreign (renderForeignError) (renderForeignError) import Node.Express.Request (getBody, getRouteParam) (getBody, getRouteParam) import SimpleService.Types -- previous code createUser :: forall eff . PG.Pool -> Handler ( postgreSQL :: PG.POSTGRESQL | eff) effeff) = getBody >>= case _ of createUser poolgetBody Left errs -> respond 422 { error : intercalate ", " $ map renderForeignError errs} errsrespondintercalaterenderForeignError errs} Right u @ ( User user) -> user) if user . id <= 0 user then respond 422 { error : "User ID must be positive: " <> show user . id } responduser else if user . name == "" username then respond 422 { error : "User name must not be empty" } respond else do $ flip P.insertUser u) liftAff (PG.withConnection poolP.insertUser u) 201 respondNoContent

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 database 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 confirm 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 } instance genericUserPatch :: Generic UserPatch _ derive instance decodeUserPatch :: Decode UserPatch where = genericDecode $ defaultOptions { unwrapSingleConstructors = true } decodegenericDecodedefaultOptions { unwrapSingleConstructorstrue }

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) (unNullOrUndefined) -- previous code updateUser :: forall eff . PG.Pool -> Handler ( postgreSQL :: PG.POSTGRESQL | eff) effeff) = getRouteParam "id" >>= case _ of updateUser poolgetRouteParam Nothing -> respond 422 { error : "User ID is required" } respond Just sUserId -> case fromString sUserId of sUserIdfromString sUserId Nothing -> respond 422 { error : "User ID must be positive: " <> sUserId } respondsUserId } Just userId -> getBody >>= case _ of userIdgetBody Left errs -> respond 422 { error : intercalate ", " $ map renderForeignError errs} errsrespondintercalaterenderForeignError errs} Right ( UserPatch userPatch) -> case unNullOrUndefined userPatch . name of userPatch)unNullOrUndefined userPatchname Nothing -> respondNoContent 204 respondNoContent Just userName -> if userName == "" userNameuserName then respond 422 { error : "User name must not be empty" } respond else do <- liftAff $ PG.withConnection pool \conn -> PG.withTransaction conn do savedUserliftAffPG.withConnection pool \connPG.withTransaction conn >>= case _ of P.findUser conn userId Nothing -> pure Nothing Just ( User user) -> do user) let user' = User (user { name = userName }) user'(user { nameuserName }) P.updateUser conn user' pure $ Just user' user' case savedUser of savedUser Nothing -> respond 404 { error : "User not found with id: " <> sUserId } respondsUserId } Just user -> respond 200 (encode user) userrespond(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’s name is present in the patch, we validate it for non-emptiness. Then, within a database 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) , delete, get, http, listenHttp, post, useExternal) import Node.Express.Types ( EXPRESS , Method (..)) (..)) import SimpleService.Handler (createUser, deleteUser, getUser, updateUser) (createUser, deleteUser, getUser, updateUser) -- previous code app :: forall eff . PG.Pool -> App ( postgreSQL :: PG.POSTGRESQL | eff) effeff) = do app pool useExternal jsonBodyParser "/v1/user/:id" $ getUser pool getgetUser pool "/v1/user/:id" $ deleteUser pool deletedeleteUser pool "/v1/users" $ createUser pool postcreateUser pool "/v1/user/:id" $ updateUser pool patchupdateUser pool where = http ( CustomMethod "patch" ) patchhttp (

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) effeff) = liftAff (PG.withConnection pool P.listUsers) >>= encode >>> respond 200 listUsers poolliftAff (PG.withConnection pool P.listUsers)encoderespond

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

-- previous code import SimpleService.Handler (createUser, deleteUser, getUser, listUsers, updateUser) (createUser, deleteUser, getUser, listUsers, updateUser) -- previous code app :: forall eff . PG.Pool -> App ( postgreSQL :: PG.POSTGRESQL | eff) effeff) = do app pool useExternal jsonBodyParser "/v1/user/:id" $ getUser pool getgetUser pool "/v1/user/:id" $ deleteUser pool deletedeleteUser pool "/v1/users" $ createUser pool postcreateUser pool "/v1/user/:id" $ updateUser pool patchupdateUser pool "/v1/users" $ listUsers pool getlistUsers pool where = http ( CustomMethod "patch" ) patchhttp (

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. Discuss this post in the comments.