REST API client recommendations?

We have a PS codebase that makes use of a REST API and I was wondering if this can be more streamlined in Purescript. Currently what bothers me is the disassociation of URL and the data that is sent over.

Although I don’t know much about polysemy, this post Polysemy, one year later | Hetchr Blog mentions interfacing HTTP APIs and the sample code here polysemy-http: Polysemy Effect for Http-Client looks quite nice.

What is your experience with such REST clients and what are your recommendations for keeping the code clean?

1 Like

What do you mean by this? Can you give an example?

I have a get method:

get :: forall a. SimpleJSON.ReadForeign a => String -> Aff a

I also have various “actions” to hardcode urls:

data Actions = GetUsers | CreateUser | Teams | ...

I can add endpoints like this

url GetUsers = "/users"
url CreateUser = "/users"
url Team = "/teams"

However I have to manually:

  1. specify the HTTP method (GET, POST, …)
  2. match action with data
    So in theory it’s possible that I have a bug like this:
fetchUsers :: Aff (Array User)
fetchUsers = get (url Teams) 

Notice that I mistyped the Action. This compiles but results in bad AP requests.

I guess this was the reason to write the Swagger client library to automatically derive all this from the URL specification. I’m wondering whether something like this exists, given that (like I presume) PureScript is more widely used on the frontend side, i.e. as a client, and not for server code (where Haskell has a bigger ecosystem).

1 Like

You could use such a trick:

data Action a 
  = GetUsers (Users -> a) 
  | GetTeams (Teams -> a)

url :: ∀ a. Action a -> String
url =
  case _ of
    GetUsers _ -> "/users"
    GetTeams _ -> "/teams"

get :: ∀ a. Action a -> Aff a
get action = makeRequest (url action)

fetchUsers :: Aff Users
fetchUsers = get (GetTeams identity) -- will error

You can take that a step further and make the definition of actions succinct (scroll to the end), at the cost of loads of fluff to assist it:

data ActionSpec resp path method as
  = ActionSpec (as ~ (Proxy (ActionSpec resp path method)))

class (IsSymbol p) <= Path as p | as -> p
instance IsSymbol path => Path (Proxy (ActionSpec resp path method)) path

class Method as m | as -> m
instance Method (Proxy (ActionSpec resp path method)) method

class (SimpleJSON.ReadForeign r) <= Response as r | as -> r
instance SimpleJSON.ReadForeign resp => Response (Proxy (ActionSpec resp path method)) resp

class (Path as p, Method as m, Response as r) <= ActionSpec' as r p m | as -> r p m
  ( Path as path
  , Method as method
  , Response as resp
  ) => ActionSpec' as resp path method

url :: ∀ as path. Path as path => Action as -> String
url _ = reflectSymbol (Proxy :: Proxy path)

getAction :: ∀ as path resp. ActionSpec' as resp path GET => Action as -> Aff resp
getAction action = get (url action)

data Action as
  = GetUsers (ActionSpec Users "/users" GET as)
  | GetTeams (ActionSpec Teams "/teams" GET as)

-- inferred: getUsers :: Aff Users
getUsers = getAction $ GetUsers (ActionSpec identity)


Couldn’t the fluff live in it’s own lib?

1 Like

Cool. Using Leibnitz is more appropriate, of course.