Migrating to elm/http 2.0


#1

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

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


#2

I’m not completely sure what you are trying to do here, but note that just a number is valid json.


#3

OMG, you are complete right! It is not needed at all.
I adapted my text, thanks.


#4

Thank you for this post, it was helpful in figuring out how to upgrade to http 2.0. I must say that deleting elm.json and rebuilding it from scratch is a pain when you have even a handful of dependencies and all you need is to update one of them.


#5

I made a simple CLI tool in rust to take care of this process in a single command. It’s not terribly sophisticated right now, but for anyone interested:


I’d appreciate any and all feedback.


#6

Great tool! I will definitively use at some point to migrate old projects. Thank you!