6.2

Fetching Data Using GET

In this section, we will try to understand the process a typical Elm app goes through to retrieve data from an HTTP server. We’ll first create a simple HTTP server on our local computer. After that, we’ll build an Elm app that will fetch data from that server.

Creating a Local HTTP Server

There are many different ways to create an HTTP server on our local computer. We’ll use the NPM package called http-server, which allows us to serve static files. Static files are files that are served to the user exactly as they are stored, without any changes due to the user’s input or preferences. Go ahead and install it globally using the -g option so that it can be run from anywhere in the terminal.

$ npm install http-server -g

Now create a file called old-school.txt inside a new directory called server, which should be placed in the root directory (beginning-elm).

Add the following text to the old-school.txt file.

We are ready to start an HTTP server. Run the following command from the beginning-elm directory in terminal.

$ http-server server -a localhost -p 5016

You should see an output like this:

Starting up http-server, serving server
Available on:
  http://localhost:5016
Hit CTRL-C to stop the server

The http-server command creates, you guessed it, an HTTP server. We give it the name of the directory to serve files from. In our case it’s server. The -a localhost option makes the URL look nicer. Without it, we would have to specify the IP address of the computer we are coding on like this example: http://127.0.0.1:5016. The -p 5016 runs the server on port 5016.

By default, an HTTP server runs on port 8080. Since your computer might be running some other application that already uses that port, it’s better to run our server on a different port to avoid a conflict. Port 5016 is rarely used by other applications.

If you go to the url http://localhost:5016/old-school.txt on a browser, you should see the contents of the old-school.txt file.

This means our local HTTP server is working. Next we’ll write some Elm code to communicate with this server.

Fetching Data from an HTTP Server

Elm provides a module called Http for sending and receiving data from a server. We installed this module in the Installing a Package section by running the following command.

$ elm-package install elm-lang/http

If you don’t have it installed already, go ahead and run the above command from the beginning-elm directory in terminal. Don’t run it from the same terminal window where we ran the http-server command earlier. Create a new one. elm-package will ask for your permission. Answer y and approve the upgrade plan it proposes.

Here is our strategy: we will first write a simple Elm program to retrieve the contents of the old-school.txt file. We will then parse this comma-separated string to extract the nicknames of the main characters from Old School — a cult classic — and display those nicknames on a page.

Let’s start by creating a new file called HttpExamples.elm in the beginning-elm/elm-examples directory.

Model

As usual, the first thing we will define is our model. Add the following code to HttpExamples.elm.

module HttpExamples exposing (..)


type alias Model =
    List String

Here is how the string we’ll be fetching from the server looks: "The Godfather, The Tank, Beanie, Cheese". We will extract each nickname and store it in a list. That’s why our model’s type is List String.

View

Next we’ll write code for displaying nicknames. Add the following code to the bottom of HttpExamples.elm.

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick SendHttpRequest ]
            [ text "Get data from server" ]
        , h3 [] [ text "Old School Main Characters" ]
        , ul [] (List.map viewNickname model)
        ]


viewNickname : String -> Html Msg
viewNickname nickname =
    li [] [ text nickname ]

Although our view is quite simple, let’s briefly go through each element. First, we display a button that when clicked tells the Elm Runtime to dispatch SendHttpRequest message to the update function — we’ll implement SendHttpRequest and update in a moment. Then we add a heading followed by an unordered (bulleted) list of nicknames.

The code for rendering each nickname is extracted out to a separate function called viewNickname. It’s a common practice in Elm to render an individual item in a list using a separate function. List.map applies the viewNickname function to each nickname string in our model to produce a list of li tags. All functions in view are defined in the Html and Html.Events modules. Let’s import them under the module declaration in HttpExamples.elm.

module HttpExamples exposing (..)

import Html exposing (..)
import Html.Events exposing (onClick)
.
.

Update

Next up is the update function and message type. Add the following code to the bottom of HttpExamples.elm.

type Msg
    = SendHttpRequest


url : String
url =
    "http://localhost:5016/old-school.txt"


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SendHttpRequest ->
            ( model, Http.send DataReceived (Http.getString url) )

Notice how similar the above update function is to the one we defined in the Commands section. Here it is once again for comparison:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        GenerateRandomNumber ->
            ( model, Random.generate NewRandomNumber (Random.int 0 100) )

The expressions for generating commands in both cases have a similar pattern. The following diagram illustrates that pattern.

Here is what the Http.send function’s type signature looks like:

send : (Result Error a -> msg) -> Request a -> Cmd msg

And here is Random.generate’s type signature for comparison:

generate : (a -> msg) -> Generator a -> Cmd msg

Like Random.generate, Http.send takes two arguments. The first argument is a function that takes a Result value and wraps it in a message. In our case, that message is DataReceived. We haven’t defined that message yet. Let’s do that by appending it to the Msg type in HttpExamples.elm.

type Msg
    = SendHttpRequest
    | DataReceived (Result Http.Error String)

DataReceived’s definition looks slightly more complex than NewRandomNumber’s from the Commands section:

type Msg
    = GenerateRandomNumber
    | NewRandomNumber Int

The command for generating a random number always succeeds. We are guaranteed to receive a random number from the Elm Runtime when asked. That’s why NewRandomNumbers definition is so simple. In contrast, fetching data from a server can fail. Perhaps the server isn’t available or the URL we’re trying to reach is incorrect. There are many other reasons why fetching data from a server may fail.

Http.send

Unlike Random.generate, the Http.send function must account for the failure scenario describe above. As we learned in the Type System section, Elm has a built-in type called Result for representing the outcome of an operation that can fail.

type Result error value
    = Ok value
    | Err error

It accepts two arguments: error and value. In our case, the type of error is Http.Error and the type of value is String.

DataReceived (Result Http.Error String)

Http.Error is yet another union type with the following data constructors.

type Error
    = BadUrl String
    | Timeout
    | NetworkError
    | BadStatus (Response String)
    | BadPayload String (Response String)

Whenever an HTTP request fails, we can expect to receive one of these values as DataReceived’s payload. We will cover the Response type later in this chapter. If the request is successful, DataReceived’s payload will be a string.

Http.Request

The second argument to the Http.send function is an HTTP request as shown in its type signature:

send : (Result Error a -> msg) -> Request a -> Cmd msg

The easiest way to create a request is by using the Http.getString function. As shown in the type signature below, it takes a URL string and returns a Request.

getString : String -> Request String

The Request type is defined in the Http module like this:

type Request a =
    Request (RawRequest a)


type alias RawRequest a =
    { method : String
    , headers : List Header
    , url : String
    , body : Body
    , expect : Expect a
    , timeout : Maybe Time
    , withCredentials : Bool
    }

We will explore this definition in detail later, but for now all we need to understand is that the URL we passed to Http.getString is assigned to the url property in RawRequest record. Additionally, the value GET is assigned to the method property since we would like to make an HTTP GET request.

Handling DataReceived Message

We need to tell the update function what to do when it receives the DataReceived message. Add a new case branch in the update function as shown below.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SendHttpRequest ->
            ( model, Http.send DataReceived (Http.getString url) )

        DataReceived result ->
            case result of
                Ok nicknamesStr ->
                    let
                        nicknames =
                            String.split "," nicknamesStr
                    in
                        ( nicknames, Cmd.none )

                Err httpError ->
                    ( model, Cmd.none )

All we are doing here is unpacking the result payload that rides on DataReceived’s back. If it’s a successful response, we use the String.split function to extract the individual nicknames from a string into a list and return that list as an updated model. If the response is an error, we simply return the existing model. We’ll write proper error handling code later.

Notice how we have managed to cram a case expression inside another case expression in the update function. For better code readability, update the nesting case expressions using pattern matching like this:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SendHttpRequest ->
            ( model, Http.send DataReceived (Http.getString url) )

        DataReceived (Ok nicknamesStr) ->
            let
                nicknames =
                    String.split "," nicknamesStr
            in
                ( nicknames, Cmd.none )

        DataReceived (Err _) ->
            ( model, Cmd.none )

In Elm, tuples can be used to write concise yet clear case expressions by matching complex patterns as we have done in the above refactoring. We’ve also replaced the payload httpError with _ because we aren’t using it. It’s best practice to replace all unused parameters with _ in Elm.

With its two essential arguments in place, the Http.send function is ready to fire off a command. The following diagram shows how various components in our app interact with the Elm Runtime to accomplish the task of fetching nicknames from a server.

Before we move on, we need to import the Http module in HttpExamples.elm. All HTTP related functions and types we have used thus far are defined in that module.

module HttpExamples exposing (..)

import Html exposing (..)
import Html.Events exposing (onClick)
import Http
.
.

Putting Everything Together

Even after writing all that code, we still don’t have a working app. Let’s wire everything together by adding the main function to the bottom of HttpExamples.elm.

main : Program Never Model Msg
main =
    program
        { init = ( [], Cmd.none )
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }

Instead of creating a separate init function, we directly assigned a tuple containing an empty list of nicknames and commands to the init property in main. It doesn’t make sense to create a separate function if all it’s going to do is return empty values, even though we did exactly that in the Commands section.

Finally, we’re ready to taste the fruits of our labor. Fire up elm-reactor from the beginning-elm directory in terminal if it’s not running already, and go to this URL in your browser: http://localhost:8000/elm-examples/HttpExamples.elm. You should see a view that looks like this:

Unfortunately, if you click the Get data from server button right now nothing happens. What could have gone wrong? To find out open the browser console.

Opening browser console
Instructions for opening the browser console depends on which browser you’re using. Please read this nice tutorial from WickedlySmart that explains how to open the console on various browsers.

When you open the browser console you should see an error message that looks like this:

What the browser is trying to tell us is that we can’t request data from a domain that’s different from the one where the request was originated from. For security reasons, most modern browsers restrict cross-origin HTTP requests initiated through an Ajax call. The Elm Runtime uses Ajax to send all HTTP requests under the hood. That’s why we weren’t able to fetch the nicknames.

At this point you may be wondering why we received that error when the local server domain and the client app domain are exactly the same: localhost. As it turns out, the cross-origin policy dictates that it’s not enough for the domains to be the same. The ports also have to match, but our server and client app are running on different ports.

Allowing Cross-Origin Resource Sharing

How do we fix this cross-origin issue? A solution is lurking in the error message. If you look closely, the browser is telling us what it expects: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Access-Control-Allow-Origin is one of the headers included in a response sent by the server. It indicates which domains are allowed to use the response. For example, if the server returns Access-Control-Allow-Origin: *, the response can be used by any domain. But if the server wants only certain domains to have access then it’ll return a domain name(s) instead of *. Here’s an example: Access-Control-Allow-Origin: http://localhost:8000.

If you check out the documentation for the http-server package we used earlier to create a local server, you’ll notice that there is an option called --cors that enables Cross-Origin Resource Sharing (CORS) via the Access-Control-Allow-Origin header. Let’s stop our local server by pressing Ctrl + c and then restart it using the --cors option.

$ http-server server -a localhost -p 5016 --cors

Now if you refresh the page located at http://localhost:8000/elm-examples/HttpExamples.elm and click the Get data from server button, you should see the nicknames of some of the most popular characters in American fraternity culture.

The --cors option adds Access-Control-Allow-Origin: * to the list of response headers. If you are using the Chrome browser, you can verify the presence of that header by following these steps:

Step #1: Refresh the page located at http://localhost:8000/elm-examples/HttpExamples.elm.

Step #2: Go to the Network tab in Developer Tools window.

Step #3: Click the Get data from server button from our app.

Step #4: A new row should appear in the Network tab. Click old-school.text below the Name column.

Step #5: Look for Access-Control-Allow-Origin in the Response Headers section.

Handling HTTP Errors

Earlier in this section, we cheated by simply returning an existing model when a request to fetch nicknames failed.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        DataReceived (Err _) ->
            ( model, Cmd.none )

We’ll rectify that by showing a proper error message.

Storing an Error Message

The first thing we need to do is store an error message in our Model. Right now it’s just a list of strings.

type alias Model =
    List String

We could simply append the error message to this list and be done with it, but that seems a bit hacky. A better alternative is to store it separately from nicknames. Let’s change our Model to use a record instead.

type alias Model =
    { nicknames : List String
    , errorMessage : Maybe String
    }

Notice the type of errorMessage. It’s Maybe String instead of just String. That’s because if the HTTP request is successful, there won’t be any error to show. What we need is a data structure that can represent the absence of a value. Maybe fits the bill.

Fixing Compiler Errors

Changing the structure of our Model causes the Elm compiler to throw errors when we refresh the page at http://localhost:8000/elm-examples/HttpExamples.elm. We can actually use those errors as a guide to figure out what needs to be fixed. This is a big advantage Elm has over other languages that don’t have a robust type system. We can count on the Elm compiler to catch our mistakes — no matter how subtle — as we mercilessly refactor our code.

Let’s start with the view function. Nicknames are now located inside a record, so we need to use the dot syntax to access them. Modify the line that contains the ul tag in view to this:

view : Model -> Html Msg
view model =
    div []
        .
        .
        , ul [] (List.map viewNickname model.nicknames)
        ]

Next we’ll fix the update function. Right now it splits the nicknames string into a list and returns that as a model. We can’t do that anymore. We need to assign the list to the nicknames property inside the model. Modify the DataReceived (Ok nicknamesStr) branch in update to this:

DataReceived (Ok nicknamesStr) ->
    let
        nicknames =
            String.split "," nicknamesStr
    in
        ( { model | nicknames = nicknames }, Cmd.none )

The only thing remaining to fix is the value we’re assigning to the init property in main. Change it to the following.

main : Program Never Model Msg
main =
    program
        { init = ( { nicknames = [], errorMessage = Nothing }, Cmd.none )
        .
        .

Inlining an initial model like that makes our code look a bit clunky. Let’s extract it out to a separate function.

init : ( Model, Cmd Msg )
init =
    ( { nicknames = []
      , errorMessage = Nothing
      }
    , Cmd.none
    )


main : Program Never Model Msg
main =
    program
        { init = init
        .
        .

And we’re back to having a working app. Refresh the page at http://localhost:8000/elm-examples/HttpExamples.elm and click the Get data from server button to make sure that you can successfully fetch the nicknames.

Displaying an Error Message

Here is our plan for notifying the users when things go haywire: if the request to fetch nicknames is a success, we’ll display a heading and a bulleted list of nicknames. However, if the request fails, we’ll replace the heading and nicknames with an error message. We can accomplish that by modifying our view code in HttpExamples.elm as shown below.

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick SendHttpRequest ]
            [ text "Get data from server" ]
        , viewNicknamesOrError model
        ]


viewNicknamesOrError : Model -> Html Msg
viewNicknamesOrError model =
    case model.errorMessage of
        Just message ->
            viewError message

        Nothing ->
            viewNicknames model.nicknames


viewError : String -> Html Msg
viewError errorMessage =
    let
        errorHeading =
            "Couldn't fetch nicknames at this time."
    in
        div []
            [ h3 [] [ text errorHeading ]
            , text ("Error: " ++ errorMessage)
            ]


viewNicknames : List String -> Html Msg
viewNicknames nicknames =
    div []
        [ h3 [] [ text "Old School Main Characters" ]
        , ul [] (List.map viewNickname nicknames)
        ]


viewNickname : String -> Html Msg
viewNickname nickname =
    li [] [ text nickname ]

We’re using separate functions to render an error message and nicknames. The core logic for rendering nicknames hasn’t changed at all. We just extracted that logic out to the viewNicknames function. The viewError function accepts an error message which will be determined later when we deal with the Http.Error value. We render that error message right below a heading.

Creating an Error Message

When a request to fetch nicknames fails, the update function is notified with a value of type Http.Error which lays out all the different ways a request can fail.

type Error
    = BadUrl String
    | Timeout
    | NetworkError
    | BadStatus (Response String)
    | BadPayload String (Response String)

See the official documentation to understand what each data constructor in Http.Error’s definition means.

Add a function called createErrorMessage right below update. This new function determines what the error message should be in each failure case.

createErrorMessage : Http.Error -> String
createErrorMessage httpError =
    case httpError of
        Http.BadUrl message ->
            message

        Http.Timeout ->
            "Server is taking too long to respond. Please try again later."

        Http.NetworkError ->
            "It appears you don't have an Internet connection right now."

        Http.BadStatus response ->
            response.status.message

        Http.BadPayload message response ->
            message

If a data constructor has a payload, an error message is created based on what’s inside it. Otherwise, we just hard code it. Now apply createErrorMessage in update to set the errorMessage property inside our model as shown below.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        DataReceived (Err httpError) ->
            ( { model
                | errorMessage = Just (createErrorMessage httpError)
              }
            , Cmd.none
            )

We replaced _ with httpError in the DataReceived (Err httpError) -> branch because we’re now actually using the error payload. We also wrapped the return value from createErrorMessage in Just because the errorMessage property expects a Maybe type. We’re ready to test the error handling code. Let’s change the URL to something invalid in HttpExamples.elm.

url : String
url =
    "http://localhost:5016/invalid.txt"

If you refresh the page at http://localhost:8000/elm-examples/HttpExamples.elm and click the Get data from server button, you should see the following error message.

Change the URL back to http://localhost:5016/old-school.txt. We won’t test other error types here, but if you ever receive one you now know how to handle it.

url : String
url =
    "http://localhost:5016/old-school.txt"

Summary

We need to go through three steps to retrieve data from a server:

1. Specify where the data should be retrieved from. We used the Http.getString function to create a request that contained the description of how and where the data should be retrieved from.

2. Retrieve data. Sending and receiving data from a server causes side effects. Since Elm functions aren’t allowed to have any side effects, our application code can’t retrieve data by itself. It needs to ask the Elm Runtime to do that by sending a command. We used the Http.send function to wrap our request in a command and handed that over to the runtime. The runtime executed that command to retrieve data from our local server.

3. Notify the update function. If the request is successful, the runtime sends the DataReceived message to update with retrieved data as a payload. If the request fails, the payload is an error instead of the data we want.

The sequence diagram below shows how the interaction between the Elm Runtime and our code looks while fetching data. Notice how similar the interaction is to the process of generating random numbers shown in the Commands section.

In the next section, we will explore how to retrieve and decode JSON data from a server. Here is the entire code from HttpExamples.elm:

module HttpExamples exposing (..)

import Html exposing (..)
import Html.Events exposing (onClick)
import Http


type alias Model =
    { nicknames : List String
    , errorMessage : Maybe String
    }


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick SendHttpRequest ]
            [ text "Get data from server" ]
        , viewNicknamesOrError model
        ]


viewNicknamesOrError : Model -> Html Msg
viewNicknamesOrError model =
    case model.errorMessage of
        Just message ->
            viewError message

        Nothing ->
            viewNicknames model.nicknames


viewError : String -> Html Msg
viewError errorMessage =
    let
        errorHeading =
            "Couldn't fetch nicknames at this time."
    in
        div []
            [ h3 [] [ text errorHeading ]
            , text ("Error: " ++ errorMessage)
            ]


viewNicknames : List String -> Html Msg
viewNicknames nicknames =
    div []
        [ h3 [] [ text "Old School Main Characters" ]
        , ul [] (List.map viewNickname nicknames)
        ]


viewNickname : String -> Html Msg
viewNickname nickname =
    li [] [ text nickname ]


type Msg
    = SendHttpRequest
    | DataReceived (Result Http.Error String)


url : String
url =
    "http://localhost:5016/old-school.txt"


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SendHttpRequest ->
            ( model, Http.send DataReceived (Http.getString url) )

        DataReceived (Ok nicknamesStr) ->
            let
                nicknames =
                    String.split "," nicknamesStr
            in
                ( { model | nicknames = nicknames }, Cmd.none )

        DataReceived (Err httpError) ->
            ( { model
                | errorMessage = Just (createErrorMessage httpError)
              }
            , Cmd.none
            )


createErrorMessage : Http.Error -> String
createErrorMessage httpError =
    case httpError of
        Http.BadUrl message ->
            message

        Http.Timeout ->
            "Server is taking too long to respond. Please try again later."

        Http.NetworkError ->
            "It appears you don't have an Internet connection right now."

        Http.BadStatus response ->
            response.status.message

        Http.BadPayload message response ->
            message


init : ( Model, Cmd Msg )
init =
    ( { nicknames = []
      , errorMessage = Nothing
      }
    , Cmd.none
    )


main : Program Never Model Msg
main =
    program
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }
Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required
Close