As many of you probably do, I wanted to upgrade my Elm application to use the new elm/http 2.0 packages. So today I bit the bullet, and did just that. I thought it might be interesting for others, so here we go.
I used elm/http 1.0, NoRedInk/elm-rails and lukewestby/elm-http-builder throughout the existing code.
This was also an opportunity to streamline this  .
.
elm.json
I moved the existing elm.json out of the way, and proceeded with elm init to make a brand new one.
I did this because changing the file by hand tends to lead to a lot of issues.
I also remove elm-stuff, just to be sure.
elm install elm/http (yes! version 2.0), and then installed all my other packages.
Code changes
I used a certain pattern a lot in my code. I would have a function that would return a Http.Request that I would Http.send in another module. It took me a while to find a way to keep this flexibility.
load: Int -> Http.Request Order
load id =
  decoder
  |> Http.get ("/orders/" ++ String.fromInt id)
init: Flags -> (Model, Cmd Msg)
init f =
  (
    { order_id = f.id},
     load f.id |> Http.send Loaded
  )
In the same file, this is easy and it just becomes:
load: Int -> Cmd Msg
load id =
  Http.get 
  { url = "/orders/" ++ String.fromInt id)
  , expect= Http.expectJson Loaded decoder
  }
init: Flags -> (Model, Cmd Msg)
init f =
  (
    { order_id = f.id},
    load f.id
  )
But when the load function is in another module?
load : Int -> (Result Http.Error Order -> msg) -> Cmd msg
load id toMsg =
  Http.get 
  { url = "/orders/" ++ String.fromInt id)
  , expect= Http.expectJson toMsg decoder
  }
and in the main module:
init: Flags -> (Model, Cmd Msg)
init f =
  (
    { order_id = f.id},
    load f.id Loaded
  )
That was not so bad.
The backend we are using in this app is a Rails application.  Rails uses certain conventions, one is that for creating records you use POST, for updating PATCH or PUT and DELETE for destroying.  Instead of figuring out how to do this I started using elm-rails (elm 0.17) and later on elm-http-builder.
I switched to elm-http-builder to be able to specify the CSRF token in the header.  elm-rails requires me to install an extra npm package to find the token in the Rails-generated HTML.  I prefer to pass it to my app as a flag.  But still there were some elm-rails calls all over the place.
Most of the time the changes were in line with the changes above:
decoder |> Rails.post "url" body |> Http.send Created
to
Http.post
{ url = "url"
, body =body,
, expect = Http.expectJson Created decoder
}
This was possible because those were posts where the CSRF verification is disabled. So better ignore those and send the token as seen below.
The places we used elm-http-builder usually looked something like
HttpBuilder.post "url"
  |> withHeader "X-CSRF-Token" model.csrf
  |> withJsonBody (encode model.form)
  |> withExpect (Http.expectJson decoder)
  |> toRequest
  |> Http.send Created
Becomes
Http.request
{ method = "POST"
, url = "url"
, headers = [ Http.header "X-CSRF-Token" model.csrf ]
, body = Http.jsonBody (encode model.form)
, expect = Http.expectJson Created decoder
, timeout = Nothing
, tracker = Nothing
}
And exactly the same goes for PUT, PATCH and DELETE.
RemoteData
Kris Jenkins has the formidable package krisajenkins/remotedata, see his talk: Slaying a UI Antipattern.
My code looked like this:
get : Cmd Msg
get =
  Decode.list decode
    |> Http.get "/accountabilities.json"
    |> RemoteData.sendRequest
    |> Cmd.map Loaded
and now looks like:
get : Cmd Msg
get =
  Http.get
    { url = "/accountabilities.json"
    , expect = Http.expectJson (RemoteData.fromResult >> Loaded) (Decode.list decode)
    }
Remember the functions I used to make that would return a Http.Request and now return a Cmd msg?
load id
  |> RemoteData.sendRequest
  |> Cmd.map Loaded
now
load id (RemoteData.fromResult >> Loaded)
Returning an Int
EDIT
You may skip the next part, as @hector pointed out to me an Int is valid Json…
So the expect becomes simply expect = Http.expectJson Created Decode.int.
But I’ll leave the code here, maybe somebody will find it interesting
/EDIT
There was still one snag, some of my actions return an integer value only, not a json, just an integer.
post : Model -> Cmd Msg
post model =
  let
    decoder : Decode.Decoder Int
    decoder =
      Decode.int
  in
    HttpBuilder.post "/addresses"
      |> withHeader "X-CSRF-Token" model.csrf
      |> withJsonBody (encode model.form)
      |> withExpect (Http.expectJson decoder)
      |> toRequest
      |> Http.send Created
now becomes
post : Model -> Cmd Msg
post model =
    Http.request
      { method = "POST"
      , url = "/addresses"
      , headers = [ Http.header "X-CSRF-Token" model.csrf ]
      , body = Http.jsonBody (encode model.form)
      , expect = expectInt Created
      , timeout = Nothing
      , tracker = Nothing
      }
Wait, where is that expectInt coming from?
expectInt : (Result Http.Error Int -> msg) -> Http.Expect msg
expectInt toMsg =
    Http.expectStringResponse toMsg <|
        \response ->
            case response of
                Http.GoodStatus_ metadata body ->
                    case Json.decodeString Json.int body of
                        Ok value ->
                            Ok value
                        Err err ->
                            Err (Http.BadBody (Json.errorToString err))
                Http.BadStatus_ metadata body ->
                    Err (Http.BadStatus metadata.statusCode)
                Http.BadUrl_ url ->
                    Err (Http.BadUrl url)
                Http.Timeout_ ->
                    Err Http.Timeout
                Http.NetworkError_ ->
                    Err Http.NetworkError
I hope this will be useful.
Herman