Fetch data with Elm from Json Placeholder

November 19, 2019 ยท 5 minute read

In this blog post I am going to walk through fetching data from a JSON API with Elm. When I started learning Elm this was a pain point for me. I have also been trying to introduce others to Elm and I haven't found enough complete examples to give to people.

This post will use the latest Elm version 0.19.1.

I will use the jsonplaceholder API and grab a list of posts from /posts.

The JSON looks like this:

        "userId": 1,
        "id": 1,
        "title": "some title",
        "body": "some body text"

Records and Decoders

Let's first deal with what our data will look like. We will need a post record.

type alias Post =
    { userId : Int
    , id : Int
    , title : String
    , body : String

And then a list of posts.

type alias Posts =
    List Post

Now when we fetch this data from the API we will need to parse the JSON into something Elm will understand. We do this by decoding the JSON.

This was really strange to me coming from JavaScript land, but once you get used to decoding your JSON you begin to see its benefits. In this example we are parsing the data fields into primitive types like Int and String. In other cases you might want to constrain your data even more and parse the data into an Elm type.

For example, I could have an endpoint that turned on a light with a status field.

    "status": "on"

    "status": "off"

The only two values state could be is On and Off. You could make a type and only accept those two values.

type Status
    = On
    | Off

Something like "status": "blue" would be invalid and you would get a nice error message and a safe way to deal with the error.

Anyway, back to our simple decoder.

We first need to install the Elm decoder package.

elm install elm/json

And now import the package in our Elm code so we can use it.

import Json.Decode as D exposing (Decoder, field, int, string)

Now we can define our decoder for the Post record we defined above.

postDecoder : Decoder Post
postDecoder =
    D.map4 Post
        (D.field "userId" D.int)
        (D.field "id" D.int)
        (D.field "title" D.string)
        (D.field "body" D.string)

Since we will be fetching a list of these posts we need another decoder to decode the list. Fortunately decoders compose really nicely.

postsDecoder : Decoder Posts
postsDecoder =
    D.list postDecoder

Define our app Model

Here I am going to use the package krisajenkins/remotedata instead of using just the elm/http package like in the official Elm docs. The RemoteData packages provides some extra types and helpers on top of elm/http.

elm install elm/http
elm install krisajenkins/remotedata

And again like before, import the things.

import Http exposing (expectJson)
import RemoteData exposing (RemoteData(..), WebData)

And now we can define our application model.

type alias Model =
    { posts : WebData Posts }

initialModel : Model
initialModel =
    { posts = Loading }

Notice how our model is a WebData Posts instead of just Posts. In VueJS I would declare the data as undefined and then when I succeed in fetching my data set the value. I would then have to check that posts is not undefined when I attempt to use the data.

Elm deals with this uncertainty in a different way. This is one of the reasons why Elm has no runtime errors.

Also notice how the initial state of the model is set to Loading. This is a type provided by the RemoteData package. The definition looks like the following:

type RemoteData e a
    = NotAsked
    | Loading
    | Failure e
    | Success a

We initialize our app in the Loading state because we will fire off the request right when the application starts up.

When we get to the view function later on we will have to deal with each of these cases NotAsked, Loading, Failure, and Success.

Define the update

Now we need to actually make the request. In Elm all side effects are dealt with in the update function. Read more about The Elm Architecture if you want to know more about how that works.

You cannot just fire off an AJAX call anywhere in the code like in JavaScript. This might seem like a nuisance, but as your web app grows this constraint makes it easy to find where your data is coming and going in your app.

type Msg
    = PostsResponse (WebData Posts)

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        PostsResponse response ->
            ( { model | posts = response }
            , Cmd.none

getPosts : Cmd Msg
getPosts =
        { url = "https://jsonplaceholder.typicode.com/posts"
        , expect =
                (RemoteData.fromResult >> PostsResponse)

Now somewhere in the code we need to fire off this getPosts command. We will do that in the app initialization. The init function takes an initial model and an initial command message.

main : Program () Model Msg
main =
        { init = \_ -> ( initialModel, getPosts )
        , view = view
        , update = update
        , subscriptions = subscriptions

View the posts

Lastly we need to be able to view the posts should the request succeed.

viewPost : Post -> Html msg
viewPost post =
        [ class "post" ]
        [ h2 [] [ text post.title ]
        , p [] [ text post.body ]

viewPosts : List Post -> Html msg
viewPosts posts =
    div [] (List.map viewPost posts)

In order to display our list of posts, we need to account for all cases of the web request. These are the 4 cases of RemoteData that I mentioned above.

view : Model -> Html msg
view model =
    case model.posts of
        NotAsked ->
            div [] [ text "Initializing" ]

        Loading ->
            div [] [ text "Loading" ]

        Failure _ ->
            div [] [ text "Network Error" ]

        Success posts ->
            viewPosts posts

And we're done. We have fetched a list of posts.

P.S. I'm getting close to finishing my book, Elm Calculator book. I build a calculator from scratch using Elm. I go through setting up CSS, using Elm types effectively, deployment, and testing.