Simple AJAX in Purescript

I’ve recently published a small library that summarizes how I’m using affjax and simple-json in my project.

It’s called purescript-simple-ajax, and its aim is to provide beginners with a solution that works in most base cases when working with AJAX and JSON.

Basic usage

The library assumes that the user is sending and receiving JSON. Thanks to the amazing simple-json library, decoding and encoding JSON takes zero effort in a lot of cases.

The basic methods that simple-ajax exposes are:

get :: forall b. ReadForeign b => URL -> Aff (Either AjaxError b)
delete :: forall b. ReadForeign b => URL -> Aff (Either AjaxError b) 
delete_ :: URL -> Aff (Either AjaxError Unit)
post :: forall a b. WriteForeign a => ReadForeign b => URL -> Maybe a -> Aff (Either AjaxError b)
post_ :: forall a. WriteForeign a => URL -> Maybe a -> Aff (Either HTTPError Unit)

put, put_, patch and patch_ have the same signature as post and post_.

Luckily, most basic types have an instance of ReadForeign and WriteForeign.

For example here encoding and decoding is done automatically, without needing to write any additional line of code:

type Foo = { foo :: Int, bar :: Maybe Boolean }

type Baz = { quuz :: Foo }

main = do
  res <- launchAff_ $ post { foo: 2, bar: Just true }
  case res of
    Left e -> -- we will deal with errors later on.
    Right v -> logShow v.quuz -- v is parsed as a Baz in this case.

Modifying the request

Along the basic methods listed above, there are also versions which accept a custom request object.

The methods are getR, deleteR, deleteR_, postR, postR_, putR, putR_, patchR, patchR_.

The methods accept a subset of

type SimpleRequest = { headers         :: Array RequestHeader
                     , username        :: Maybe String
                     , password        :: Maybe String
                     , withCredentials :: Boolean
                     , retryPolicy     :: Maybe RetryPolicy

What this means is that if we need to pass the whole record, but just the field we need to change from the basic request. E.g.

getR { withCredentials: true } ""


The different types of error (Error, ForeignError and ResponseFormatError) are put together in a Variant, and there are two type alias that can be used:

  • HTTPError containing the HTTP errors
  • AjaxError which extends HTTPError with JSON parsing errors

Using Variant tools we have a lot of power on how to deal with these errors.

  • We can ignore the underlying type, and just handle the generic error case:
case res of
  Left _ -> log "ERROR!"
  • We can handle a default case and do something specific for the errors we are interested in:
case res of
    Left e -> 
      let mess = default "Generic error"
                 # on _notFound const "Not found"
                 $ e
      log mess
  • Or we can be sure that all errors case are handled in a specific way:
case res of
    Left e ->
      let mess = onMatch
        { badRequest: \s -> "Bad request: "  <> s
        , unAuthorized: \_ -> "Unauthorized"
        , forbidden: \_ -> "Nope"
        , notFound: \_ -> "Hello"
        , methodNotAllowed: \_ -> "!"
        , formatError: \err -> "Error: " <> renderForeignError err
        , serverError: \err -> "Server error: " <> err
        , parseError: \ers -> intercalate ", " $ map renderForeignError ers
        } e
      log mess

The possible errors are listed in the types BasicError and ParseError, along with the eventual payload they carry.


To enable retries, we can just pass a request with the retryPolicy field. E.g.

getR { retryPolicy: defaultRetryPolicy } ""

Custom instances

If the compiler complains that our data types don’t implement ReadForeign or WriteForeign, we have some options:

  • if we implemented those data types, we can just write the instances for them.
  • if we are using data types from a library, we can either wrap them in a Newtype, or write two customs functions Foo -> Foreign and Foreign -> Either MultipleErrors Foo. I prefer the second approach, as generally I only need to read/write once, and I don’t want to have to deal with wrapping and unwrapping a newtype all the time.

You can find more about this reading simple-json documentation.

Related Posts

Adding live css and js reload to Yesod

A quick overview of Purescript package managers as of October 2018

Optional elements and properties in Halogen

Automatically adding (or removing) a prefix to a record labels in Purescript

Adding static files to Yesod

Planning a simple Reddit clone with Yesod

React-router-dom bindings for Reason

Parsing complex foreign objects in PureScript

A way to deal with big objects and FFI.

Post requests with PureScript Affjax and Argonaut

Simple ajax calls to an API example

Functor, Applicative e Monad illustrati

Una guida chiara