A convenience pattern around Http?

195 views
Skip to first unread message

Rupert Smith

unread,
Sep 23, 2016, 7:45:31 AM9/23/16
to Elm Discuss
Making a REST call from Elm and geting back the results is quite a complicated process.

1. Set up a Msg that the UI can trigger the request from.
2. Write a Task to perform the request.
3. In the update function turn that into a Cmd to make the request using Task.perform.
4. Set up Msgs to handle the ok and error responses, or maybe just one Msg to handle both as a Result.
5. Deal with errors in some way.
6. Deal with the response by updating the model approriately.

I am wondering if all of this can be wrapped up in a more convenient interface that the consumer of a REST service can make use of in a simpler manner.

At the moment I am thinking:

Some convenience functions for triggering the various REST calls:

myExamplePost : SomeData -> ()

A convenience function that helps with lifting the 'update' function of the REST service within the update function of whatever module is making use of it. This will take as args:

- A function to extract from the model whatever part of the model is appropriate for updating when results are received.
- A record containing callback functions that will be invoked with results, e.g.

type alias Callbacks model msg =
    { myExampleGet : SomeData -> model -> Cmd msg
    , ...
    }

Often they will return Cmd.none, but that is put there in case other commands need to be chained.

Then the consumer of a REST service really just has to provide a set of callbacks, and has a convenient set of functions to help integrate the service, and to initiate requests. There will also need to be some callbacks for handling errors, not sure yet on the details of those.

I may be a bit approximate with the details above, but does this sound doable/reasonable? Has anyone done something along these lines already?

Rupert Smith

unread,
Sep 30, 2016, 9:37:35 AM9/30/16
to Elm Discuss
On Friday, September 23, 2016 at 12:45:31 PM UTC+1, Rupert Smith wrote:
I am wondering if all of this can be wrapped up in a more convenient interface that the consumer of a REST service can make use of in a simpler manner.

I now have this working and here is how it works.

Firstly, I define a set of call-back functions which will be notified when REST operations complete:

callbacks : Account.Service.Callbacks Model Msg
callbacks =
    { findAll = accountList
    , findByExample = accountList
    , create = \account -> \model -> ( model, Cmd.none )
    , retrieve = \account -> \model -> ( model, Cmd.none )
    , update = \account -> \model -> ( model, Cmd.none )
    , delete = \response -> \model -> ( model, Cmd.none )
    , error = error
    }

The return type lets me update the model and issue follow on commands, just like update. In fact these callbacks will be invoked from within the update function of the REST module.

In the update function of the module that wants to use it, I need a Msg for events that are handed down to the REST module:

update' : Msg -> Model -> ( Model, Cmd Msg )
update' action model =
    case action of
        ...
        AccountApi action' ->
            Account.Service.update callbacks action' model

And then to invoke a REST endpoint I have set up convenience functions to make the calls and Cmd.map the outcomes as events local to this module (which are then handed back down to the REST module):

        Init ->
            ( { model | selected = Set.empty }, Account.Service.invokeFindAll AccountApi )

I pass all errors back to just one error handler, not one per endpoint. This may prove to be a limitation in the future, but seems ok for now. Mostly I am interested in handling 401 with a logout, 404 with a not found page, and anything else with a "whoops something went wrong" generic error message. Have not done the 404 yet, but the error handling function can be typed generically enough to work with any model or message type in the generic case:

error : Http.Error -> model -> ( model, Cmd msg )
error httpError model =
    case httpError of
        Http.BadResponse 401 message ->
            ( model, Auth.logout )

        _ ->
            ( model, Cmd.none )

The use of callbacks simplifies the update function in the module using the REST module, as there is no need to define Msgs for every event resulting from the Http calls. I think the type of the Callbacks is very useful in guiding the caller as to what exactly they need to implement, whereas leaving it up to the caller to figure out what events they need to handle is much less helpful.

It does mean that the flow of control goes from the calling module to the REST module, back to the calling module, back to the REST module, so I have more code in total than I might otherwise need. On the other hand, I have now managed to code gen the full REST interface with its model, encoders/decoders and convenience fuctions to make the REST calls - so my priority was removing as much boiler plate as possible from the calling module.

The amount of boiler plate around json and REST compared with what you need in javascript does seem to be a weakness of Elm. In my adventures so far, if I had to pick an area for improvement that would help to make Elm a more popular language I think it would be this.

I am now able to talk json over REST to my services without having to hand code all this boiler plate, which feels really good. Now I can get on with the fun part of making a nice UI.

Rupert Smith

unread,
Oct 4, 2016, 11:24:45 AM10/4/16
to Elm Discuss
On Friday, September 30, 2016 at 2:37:35 PM UTC+1, Rupert Smith wrote:
Firstly, I define a set of call-back functions which will be notified when REST operations complete:

callbacks : Account.Service.Callbacks Model Msg
callbacks =
    { findAll = accountList
    , findByExample = accountList
    , create = \account -> \model -> ( model, Cmd.none )
    , retrieve = \account -> \model -> ( model, Cmd.none )
    , update = \account -> \model -> ( model, Cmd.none )
    , delete = \response -> \model -> ( model, Cmd.none )
    , error = error
    }

One addition I am making to this, is to define a default set of 'no action' callbacks in the service. Always -> ( model, Cmd.none). Then the consumer of the service just needs to override and specify which events they are interested in handling.

Kasey Speakman

unread,
Oct 4, 2016, 12:54:01 PM10/4/16
to Elm Discuss
Seems like this is trying to normalize operations to CRUD. I would be careful of over-standardizing early. Because later you will likely have to break those abstractions to implement new features. For example, the way I would implement a Customer merge would not fit semantically into one of those operations.

There are a few steps involved in creating a new code path to an API, but most of it seems pretty reasonable. And there's nothing preventing you from making helper functions if you find yourself repeating mostly the same code for different calls.

Rupert Smith

unread,
Oct 5, 2016, 5:57:12 AM10/5/16
to Elm Discuss
On Tuesday, October 4, 2016 at 5:54:01 PM UTC+1, Kasey Speakman wrote:
Seems like this is trying to normalize operations to CRUD. I would be careful of over-standardizing early. Because later you will likely have to break those abstractions to implement new features. For example, the way I would implement a Customer merge would not fit semantically into one of those operations.

Yes, what I am doing is using codegen + a data model to create a starting point for a service. You get CRUD plus finders to get things started. I also codegened a client in Elm, again giving me CRUD plus finders on the client side.

I then add new operations or remove ones that I don't want by hand. It is over standardizing but it is also incredibly useful and helps me get things up and running very fast. Find by example is particularly useful in the early stages of a project, as it can generate a good range of queries with little effort.

Eventually I will add more flexibility to the codegen to let me select which operations out of CRUD plus finders I actually want to generate plus any other useful standard operations I can think of. Also better modelling of custom operations that are hand coded, such that the codegened client does not also need those added by hand but can always be automatically made to fully match the server side API.

So for the purposes of this thread, don't get too hung up on the over standardized API. What is interesting from the point of view of Elm is how I have wrapped up the REST operations in a module and tried to minimize the amount of boilerplate needed to use them.

===

One thing I will have to change is the error handling. I need to know if individual operations fail and how - for example, when doing a create if the entity to create fails server side validation I need to know that and also get some errors from the server and be able to render those in the UI.

I think a catch-all error hander is still useful, perhaps I will try and set things up so the error goes to an endpoint specific handler first, and then on to the catch all if the specific handler does not process it.
Reply all
Reply to author
Forward
0 new messages