This is a very simple example how to send, receive and handle HTTP requests in elm. We use NoRedInk/elm-json-decode-pipeline for easier JSON decoding.
Run the local development (see below) and click on the Fixer
link in the nav bar.
After that you will need to provide the API key which you get from fixer.io.
When you provide a valid key you can get gbp and usd rates for eur.
All the logic lives in src/Fixer.elm
.
Let's review the most important parts of the code that makes this app "tick".
First, we define types of messages that our app will use.
type Msg
= SetApiKey String
| GetRates
| GotRates (Result Http.Error Fixer)
“A message is a value used to pass information from one part of the system to another.”
Excerpt From: Richard Feldman. “Elm in Action MEAP V11”. Apple Books.
We will ignore the SetApiKey String
message because it is just used to set the API Key.
Let's take a look at GetRates
message. If we want to trigger it we can put an on click
event on a button like this:
button [ Events.onClick GetRates ] [ text "Get Rates" ]
And we need to handle this message case in our update
function:
GetRates ->
( { model | fixer = Loading }
, getRates model
, Cmd.none
)
So when a user clicks on the Get Rates
button the model will change and a function will
get called. The model.fixer
value will become Loading
and getRates
function will
get called (with model
argument).
getRates : Model -> Cmd Msg
getRates model =
Http.get
{ url = model.endpoint ++ model.key
, expect = Http.expectJson GotRates decodeFixer
}
getRates
returns a command which needs to be type Cmd Msg
. In our case the Msg
is
GotRates (Result Http.Error Fixer)
. See HTTP docs
for more info.
In expect
we define what do we expect and how to handle that. We expect JSON and we
want to handle it with decodeFixer
function.
decodeFixer : Json.Decode.Decoder Fixer
decodeFixer =
Json.Decode.succeed buildFixer
|> required "success" Json.Decode.bool
|> required "timestamp" Json.Decode.int
|> required "base" Json.Decode.string
|> required "date" Json.Decode.string
-- because rates is its own object we need to tell elm how to decode it
|> required "rates" decodeFixerRates
decodeFixer
uses NoRedInk/elm-json-decode-pipeline
library for easier JSON decoding. You can do decoding without any library but you need
to use mapN
functions. For example:
type alias User = {id : Int, email : String, staff : Bool }
decodeUser : Decoder User
decodeUser =
map3 User
(field "id" int)
(field "email" string)
(field "staff" bool)
And if we want to add another field e.g. username
we would need to change map3
to
map4
and add (field "username" string)
at the end.
This decoding works because a record type alias can be called as a normal function
If we go back to our decoder and take a look at the last line:
|> required "rates" decodeFixerRates
This is a bit different from the previous lines where we just have int
, bool
or string
at the end (which indicates the type of the value that we want to decode). But here we
have a function called decodeFixerRates
. This is because the JSON object that we get
is something like this:
{
"base": "USD",
"date": "2018-02-13",
"rates": {
"CAD": 1.260046,
"CHF": 0.933058,
"EUR": 0.806942,
"GBP": 0.719154,
}
}
So the rates
field is another object that we need to decode and we need to tell elm
how to do this decoding. We do this by creating a new decoding function called
decodeFixerRates
:
decodeFixerRates : Json.Decode.Decoder Rate
decodeFixerRates =
Json.Decode.succeed buildRate
|> required "EUR" Json.Decode.float
|> required "GBP" Json.Decode.float
|> required "USD" Json.Decode.float
It is actually a very simple decoding function that decodes 3 fields to float types.
TIP: If you are lazy like me you can use this tool to generate encoders and decoders for a given JSON object.
Once we got all the decoding done elm runtime fires the GotRates (Result Http.Error Fixer)
message and we handle it in the update
function:
GotRates result ->
case result of
Ok fixerData -> ( { model | fixer = fixerData }, Cmd.none, Cmd.none )
Err error ->
case error of
Http.BadBody body ->
( { model | fixer = Failure body }
, Cmd.none
, Cmd.none
)
_ ->
( { model | fixer = Failure "Something went wrong" }
, Cmd.none
, Cmd.none
)
Similar to Maybe
the Result
type has two type variables:
type Result errValue okValue
= Err errValue
| Ok okValue
See the docs fore more info.
Just like with Maybe
where we need to write the logic to handle both the Just
and
Nothing
cases, so does the Result
require to handle both the Ok
and Err
cases.
The Ok
case is very straight forward. We just set the model.fixer
value to whatever
we received and decoded from the Fixer API.
In the Err
case we have several options. We can say that whenever an error (any error)
happens, we will set the model.fixer
value to Failure
with Something went wrong
message (Fixer
type has Failure String
option). For example:
Err error ->
( { model | fixer = Failure "Something went wrong" }
, Cmd.none
, Cmd.none
)
Or we can specify what happens for each specific error. In our case, we explicitly handle
the Http.BadBody
case. Here is a list
of all Error
values that you can handle.
-
npm install
-
npm run dev
src/
Components/ -- reusable bits of UI
Layouts/ -- views that render pages
Pages/ -- where pages live
Global.elm -- info shared across pages
Main.elm -- entrypoint to app