elm christmas

Chaining HTTP requests

←Previous postNext post →

A 5 min read written by
Ingar Almklov

Let's say we want to get a list of all current lords of houses in The Riverlands region of the Song of Ice And Fire universe (you know, Game of Thrones) that have not died out. There is a great API for doing exactly this, located at https://anapioficeandfire.com.

We see that it has an endpoint for houses (https://anapioficeandfire.com/Documentation#houses) that we can filter on region and hasDiedOut. The "problem" is that the API is not including the full character object in its response for the currentLord property. Instead currentLord is a URL that we can use to fetch more information about that character.

The outline of what we have to do is:

  1. Fetch all houses matching our query
  2. For each house, fetch the current lord of the house

The simplest approach would be to have a model that is something like this:

type alias Model =
    { houses : WebData (List House)}
    , currentLords : WebData (List Character)

This WebData thing comes from a great library called RemoteData that makes it easier to work with HTTP requests.

With this we could first fetch the houses, and when we get a successful response, kick off X number of commands for fetching the lords of those houses.

This will work, but in my example I don't care about the houses, I only want to know about the current lords, and I want to do it in one go. In short, I want one command:

fetchLords : Cmd Msg

To achieve this, we have to do some "chaining".


andThen is the Elm way of chaining computations that might fail. (If you have done some Haskell or similar languages you might know it as bind.)

The name andThen makes sense, as what we want to do is "first fetch houses, and then if that went OK we fetch the lords".

Since Elm's HTTP module does not have an andThen function, we have to drop down to Tasks.

To deal with HTTP requests and Tasks I like using elm-http-builder. Let's get to it.

The application

Starting with types is usually a good idea:

type alias Model =
    { lords : WebData (List Character) }

type Msg
    = FetchClick -- We fetch stuff when the user clicks a button
    | LordsFetched (WebData (List Character))

type alias Character =
    { url : String
    , name : String
    , culture : String
    , born : String
    , died : String
    , titles : List String
    , aliases : List String
    , father : String
    , mother : String
    , spouse : String
    , allegiances : List String
    , books : List String
    , povBooks : List String
    , tvSeries : List String
    , playedBy : List String

type alias House =
    { url : String
    , name : String
    , region : String
    , currentLord : String
    , swornMembers : List String

-- And we also create some decoders for characters and houses
decodeCharacter : Json.Decode.Decoder Character
decodeHouse : Json.Decode.Decoder House

When we get the FetchClick message, we want to fetch the lords

update msg model =
    FetchClick ->
        ({ model | lords = Loading }
        , fetchLords -- Remember that fetchLords is a Cmd Msg

    LordsFetched lords ->
        -- TODO

We create a helper for fetching the houses:

fetchHouses : Task Http.Error (List House)
fetchHouses =
    HttpBuilder.get "https://www.anapioficeandfire.com/api/houses"
        |> HttpBuilder.withQueryParam "pageSize" "50" -- default is only 10 results
        |> HttpBuilder.withQueryParam "region" "The Riverlands"
        |> HttpBuilder.withQueryParam "hasDiedOut" "false"
        |> HttpBuilder.withExpectJson (Json.Decode.list decodeHouse)
        |> HttpBuilder.toTask

And one for fetching the lord of a house:

fetchCurrentLord : House -> Task Http.Error Character
fetchCurrentLord house =
    HttpBuilder.get house.currentLord -- house.currentLord is a URL for that character
        |> HttpBuilder.withExpectJson decodeCharacter
        |> HttpBuilder.toTask

Now let's look at how we can combine these by using Task.andThen and Task.sequence:

fetchLords : Cmd Msg
fetchLords =
    -- First fetch all the houses mathing our query
        |> Task.andThen
            -- If fetching the houses succeeded, they are passed to this function:
            (\houses ->
                    -- For some reason, some houses have an empty string as currentLord.
                    -- We just ignore those for now.
                    |> List.filter (\house -> not <| String.isEmpty house.currentLord)
                    -- For each house, create a task for fetching the current lord
                    |> List.map fetchCurrentLord
                    -- At this point we have a List (Task Http.Error Character).
                    -- Use Task.sequence to turn it into Task Http.Error (List Character)
                    |> Task.sequence
        -- We now have a nice Task Http.Error (List Character)
        -- To turn it into a Cmd Msg we first convert to Cmd (WebData (List Character))
        |> RemoteData.asCmd
        -- And finally convert it into our Msg
        |> Cmd.map LordsFetched

Finally, fill in the update function:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    FetchClick ->
        ({ model | lords = Loading }
        , fetchLords -- Remember that fetchLords is a Cmd Msg

    LordsFetched lords ->
        ({ model | lords = lords }
        , Cmd.none

And there we have it!

Head on over to https://github.com/ingara/elm-http-chain-example to see the full code for this example.

←Previous postNext post →