Making HTTP Requests in Elm
Note: This post was written for 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
You can view the full diff of step 2 on GitHub.
#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
View the full diff of step 3 on GitHub.
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.