Making HTTP Requests in Elm
0.16
. There is now an official documentation for doing HTTP requests with the latest version.When I learn a new programming language I like to read about all its details first. Afterwards it helps reading code written in the language that does something very simple to get a feel how all these parts fit together and how the code is organized.
When I started learning Elm I read about Signals and Tasks and how to use them in the Elm architecture, but I missed a tutorial that covers building an app from the ground up that shows how to do an HTTP request and use the StartApp
package.
This is why I started writing this one while learning the language myself. I’ve built this app in three steps and published it to GitHub. In the readme you can find links to a git diff for each step that shows how the code has changed.
Goal
My goal was to create an app completely written in Elm that does the following:
- user clicks a button
- app makes an HTTP Request to jsonip.com
- it parses the JSON response
- updates the model
- and displays the received IP address
Step 1: Basic Setup
Click here to view the full diff for step 1 on Github.
Installing Elm locally via npm
You can install Elm globally, but updating it to a new version would break all projects that depend on it. That’s why I prefer to install Elm locally for each project via npm
. I started by generating a package.json
with npm init
and installed the Elm command line tool with npm install elm --save-dev
. Then inside package.json
I added a script to make it accessible via npm scripts:
{
…
"scripts": {
"elm": "elm"
}
…
}
I also like to set up a watch
command for development. You can read my post on how to compile Elm files on save without gulp
or webpack
to see how that’s done. (code is also included in the diff of step 1)
Installing the Elm packages
Then I executed these commands in terminal to install the packages the app needs:
npm run elm package install evancz/start-app
This package includes the StartApp
function, which gets you started with the Elm architecture.
npm run elm package install evancz/elm-http
The name says it all: a package for making HTTP requests in Elm.
npm run elm package install evancz/elm-html
This one is using virtual-dom to render HTML (like React does).
npm run elm package install evancz/elm-effects
The last one is needed for executing side effects in Elm.
Creating a basic Main.elm
To test if the setup works before adding interactions, I created a basic Elm file, that just renders “Hello World”:
module Main where
import Html exposing (text)
main =
text "Hello World"
Setting up the entry html file
Now I created an index.html
file, which imports the compiled Elm code. You can learn about more ways to embed Elm in the official Interop documentation.
<!DOCTYPE html>
<html>
<head>
<title>Elm HTTP Request Example</title>
<script src="/elm.compiled.js"></script>
</head>
<body>
<div id="main"></div>
<script>
Elm.embed(Elm.Main, document.getElementById("main"));
</script>
</body>
</html>
Compiling Elm
As I have previously set up a watch
command I can now run npm run watch
and save my Elm files to compile the JavaScript. Alternatively I can execute npm run elm -- make Main.elm --output elm.compiled.js
to compile them just once.
Starting a local web server
To test if everything works, I started a local web server in my directory. On Mac you can do this by running:
php -S localhost:8000
Then I opened localhost:8000
in my browser to check if “Hello World” is printed on the screen.
Step 2: Adding the StartApp package
Adding StartApp
to Main.elm
I started by splitting up my Elm code into two files. Main.elm
now only contains the small boilerplate of wiring up the Elm Architecture using StartApp
and App.elm
contains the actual app logic.
StartApp.start
expects a record with four fields:
init
: sets the initial values of theModel
and can triggerEffects
(e.g. fetching data from the backend)view
: rendersHtml
with theModel
it gets and wires up event handlers with anAddress
update
: updates theModel
and triggers sideEffects
based on theActions
it getsinput
: imports a list ofSignals
that triggerActions
The main
function changed to return a Signal Html
, which it gets from the return value of the StartApp.start
.
Ports
The last thing the Elm runtime needs to execute Effects
is a port
to run the tasks. Now my entry Elm file is set up so that the actual app code can do side effects.
Adding business logic in App.elm
Model
I defined a Model
that only has one field to store the IP as a String
.
type alias Model =
{
ip: String
}
Init
The init
function sets the initial IP to "Unknown"
without running any side effects.
init : (Model, Effects Action)
init =
(
{
ip = "Unknown"
},
Effects.none
)
Update
My initial implementation of the update
function only covers one Action
, that does nothing.
type Action
= DoNothing
update: Action -> Model -> (Model, Effects Action)
update action model =
case action of
DoNothing ->
(model, Effects.none)
View
The view
function returns a div
that contains a button
(without any event handling for now) and a div
, which renders the ip
field of the Model
.
view : Address Action -> Model -> Html
view address model =
div [] [
button [] [ text "Get IP address" ],
div [] [ text model.ip ]
]
Step 3: Make HTTP Request and decode JSON
In the last step I updated App.elm
to make the HTTP Request and deal with the JSON response.
Actions
I added two actions:
RequestIP
: triggers the HTTP RequestUpdateIP
: updates the model with the received IP address
type Action
= DoNothing
| RequestIP
| UpdateIP (Maybe String)
Update
Below is how they are handled in the update
function. The RequestIP
action triggers the requestIP
function, which is defined in the Effects section. UpdateIP
receives the ip
, but the response might not have an IP, therefore it is wrapped in a Maybe
. In case the server did not respond as expected the ip
field in the Model is set to "No response"
.
RequestIP ->
(model, requestIP)
UpdateIP ip ->
(
{ model | ip = (Maybe.withDefault "No response" ip) },
Effects.none
)
View
button
got a new attribute onClick
, which sends the RequestIP
action to StartApp
’s address.
view : Address Action -> Model -> Html
view address model =
div [] [
button [ onClick address RequestIP ] [ text "Get IP address" ],
div [] [ text model.ip ]
]
Effects
The last part of the file is doing the actual request. Http.get
sends a GET request to jsonip.com. Its second argument is a function that decodes the ip
field of the JSON response to a string.
The returned Task
of Http.get
is transformed into one that cannot fail by wrapping the result in a Maybe
. This gets wrapped by the UpdateIP
action via Task.map
. At last Effects.task
sends the task to Elm’s runtime, which actually does the request.
requestIP : Effects Action
requestIP =
Http.get ("ip" := Json.string) "http://jsonip.com"
|> Task.toMaybe
|> Task.map UpdateIP
|> Effects.task
Final words
I hope this tutorial helped you as well to better understand how side effects work in Elm. The result shows how little boilerplate code is needed in Elm compared to the current state of a typical JavaScript build process.
If you have any questions drop a comment below and I’ll try to answer them. The full code is open source on GitHub. And yes, I should use a syntax highlighter on my website that supports Elm.