In this section, we’ll 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.
Now create a file called old-school.txt
inside a new directory called server
, which should be placed in the beginning-elm
root project directory.
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.
You should see an output like this:
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: http://127.0.0.1:5016
. The -p 5016
runs the server on
port 5016
.
By default, an HTTP server runs on port 8080
. Your computer might be running some other application that already uses that port, so 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 in chapter 2 by running the following command.
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 http-server
earlier. Create a new one. When elm install
asks for your permission, answer y
.
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/src
directory.
Model
As usual, the first thing we will define is our model. Add the following code to HttpExamples.elm
.
The string from server looks like this: "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 display the nicknames. Add the following code to the bottom of HttpExamples.elm
.
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 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 those modules in HttpExamples.elm
.
Update
Next up is the update
function and message type. Add the following code to the bottom of HttpExamples.elm
.
And import the Http
module.
update
handles the SendHttpRequest
message by returning the original model and a command for fetching nicknames from the local HTTP server we created earlier. Here is what the Http.get
function’s type signature looks like:
It takes a record with two fields and returns a command. The url
field holds the location of the server resource. The expect
field specifies the format we expect from the server. By using Http.expectString
, we’re letting Elm know that we expect the response body to be a string. Here is what the Http.expectString
function’s type signature looks like:
Comparing Http.get to Random.generate
In the Commands section, we wrote the following code to generate a random number.
Although Random.generate
and Http.get
look structurally different, they both contain three ingredients required for communicating with the outside world:
- A mechanism for creating a command.
- What needs to happen when the command is run?
- Which message should be sent to the app after the command has been executed?
Comparing DataReceived to NewRandomNumber
The DataReceived
message looks slightly more complex than NewRandomNumber
.
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 NewRandomNumber
’s 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. Therefore, unlike Random.generate
, Http.get
must account for those failure scenarios.
As mentioned in the Type System section, Elm has a built-in type called Result
for representing the outcome of an operation that can fail.
It accepts two arguments: error
and value
. In our case, the type of error
is Http.Error
and the type of value
is String
.
Http.Error
is a built-in custom type with the following data constructors.
Whenever an HTTP request fails, we can expect to receive one of these values as DataReceived
’s payload. If the request is successful, DataReceived
’s payload will be a string. Check out the official documentation to find out what those error types mean.
Handling DataReceived Message
We need to tell the update
function what to do when the DataReceived
message arrives. Add a new case
branch to update
in HttpExamples.elm
to handle that message as shown below.
All we are doing here is unpacking the result
payload that rides on DataReceived
’s back. If it’s a successful response, the individual nicknames are extracted from a string into a list using String.split
and that list is returned as an updated model. If the response is an error, we simply return the existing model. We’ll write proper error handling code in the Handling HTTP Errors section below.
Notice how we have managed to cram a case
expression inside another case
expression in the update
function? We can use pattern matching to get rid of nested case
expressions like this:
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 right now. It’s best practice to replace all unused parameters with _
in Elm.
We’ve assembled all pieces required to fire an HTTP 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.
Wiring Everything Up
Even after writing all that code, we still don’t have a working app. Let’s wire everything up by adding the main
function to the bottom of HttpExamples.elm
.
We also need to import the Browser
module in HttpExamples.elm
.
Instead of creating a separate init
function, we directly assigned an anonymous function that takes flags
and returns a tuple containing an empty list of nicknames and commands to the init
field 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/src/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. 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 which uses the XMLHttpRequest
JavaScript object behind the scenes. 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.
- Server URL:
http://localhost:5016/old-school.txt
- Client URL:
http://localhost:8000/src/HttpExamples.elm
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
.
The http-server
package we used earlier to create a local server provides an option called --cors
for enabling 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.
Now open a new browser window in private mode and go to http://localhost:8000/src/HttpExamples.elm
. After that click the Get data from server button and you should see the nicknames of some of the most popular characters in American fraternity culture.
Note: Most browsers cache CORS policies for sometime in non-private mode. That’s wny we need to open HttpExamples.elm
in private mode. Otherwise, we keep getting the same CORS error we saw earlier even after refreshing the page.
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. Open the page located at http://localhost:8000/src/HttpExamples.elm
in a new private window.
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.
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.
We could simply append the error message to this list, 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.
The errorMessage
field’s type is 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/src/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:
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:
The only thing remaining to fix is the value we’re assigning to the init
property in main
. Change it to the following.
Inlining an initial model like that makes our code look a bit clunky. Let’s extract it out to a separate function.
And we’re back to having a working app. Open the page at http://localhost:8000/src/HttpExamples.elm
in a new private window 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 succeeds, 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.
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.
Add a function called buildErrorMessage
right below update
in HttpExamples.elm
. This new function determines what the error message should be in each failure case.
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 call buildErrorMessage
from update
to set the errorMessage
property inside our model as shown below.
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 buildErrorMessage
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
.
If you refresh the page at http://localhost:8000/src/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.
Summary
We need to go through three steps to retrieve data from a server:
1. Specify where to retrieve data from. We used the url
and expect
fields in the record passed to the Http.get
function to let the Elm runtime know where our data resides and in what format.
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.get
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.
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
: