DEV Community

Cover image for Dad jokes and HTTP requests in Elm
selbekk
selbekk

Posted on • Edited on • Originally published at selbekk.io

Dad jokes and HTTP requests in Elm

What does the Elm developer say when asked to go out to party? Maybe. Oh yes. Welcome to the shit show that is my dad joke game 😎

After creating both a counter and a todo app in Elm, I'm ready to go for the big leagues. Create that real business value. Satisfy the client needs in a functional fashion. So I decided to create dad joke generator via the fantastic and free Dad Joke API!

Even though the feature might be laughable (hah! 😄), it's no joke to implement - at least not for me. We'll have to do HTTP calls, set the correct headers, and deal with something called commands.

In this article, I'm going to take you through what I did step by step, and try to explain the new concepts I encounter.

Creating a good model

I like starting with modeling the state of our application. In this case, we have three possible states - waiting for data, having failed to fetch the data, and successfully fetching the data. We can model that with a regular type:


type Model
    = Failure
    | Loading
    | Success String
Enter fullscreen mode Exit fullscreen mode

I guess we could've created an Idle state as well, but we're going to fetch a joke initially anyhow, so it won't be a need for that. Speaking of...

Initializing with commands

The next thing we want to do is intializing our application. It'll be a bit different than we've done previously - instead of just being the initial model, it'll now be a function that returns something called a tuple.

The type signature looks like this:

init : () -> (Model, Cmd Msg)
Enter fullscreen mode Exit fullscreen mode

There's a few new things here. First, what's a tuple? In this case, you can think of it as way to return several values from a function. It's a bit more complex than that, but you can think of them as a very light weight data structure of sorts.

In our case though, we're just returning two things - our initial model, and an initial Cmd. But what is a Cmd?

A Cmd (or command) is something you want Elm's runtime to do for you. It can be a lot of things, like creating random numbers or accessing browser APIs. Or doing HTTP calls. Once it's resolved somehow, it will return a Msg, which our update function will deal with.

If you're coming from a React background, you can think of this Cmd argument to our init function as an action you'd want to run on mount. Kind of like componentDidMount or useEffect(fn, []) works!

So let's write our initializer!

init : () -> (Model, Cmd Msg)
init _ =
    ( Loading
    , Http.get 
        { url = "https://icanhazdadjoke.com"
        , expect = Http.expectString GotJoke 
        }
    )
Enter fullscreen mode Exit fullscreen mode

We return a tuple with our initial model - Loading, and a call to the Http.get function.

To get this to compile, you need to install the elm/http package with elm install elm/http in your terminal, and then imported in your file with import Http.

The Http.get function accepts a record with two parameters - the url, and something called expect. This last one uses another function - Http.expectString, which tells Elm to parse the response as a string, and then "dispatch" the GotJoke message with the result.

But - we haven't specified our messages yet - let's do that next.

Messages and results

For now, we only have one possible message to send in our app - namely the "we received a response from the server" value.

That "we received a response from the server" is wrapped in a Result type, which can either be an error or the successfully parsed response. We can specify that like so:

type Msg = 
    GotJoke (Result Http.Error String)
Enter fullscreen mode Exit fullscreen mode

Now, I'm no functional programming nut, but if I've understood the introductory blog posts correctly, Result is what's known as a monad. 😱 Now, after the obligatory panic attacks, I realized monads a just containers for a value that adds some functions for you to use. However, if you're new to functional programming, just close your eyes to the monad part, and just think of Result as a type that's either Ok or Err.

Implementing the update function

The update function is a bit different this time around as well. The type signature looks like this:

update : Msg -> Model -> (Model, Cmd Msg)
Enter fullscreen mode Exit fullscreen mode

Like before, the update function receives the message and the initial model, but now it's returning a tuple - two values - (Model, Cmd Msg). This feature lets a message trigger a command, if we want to.

Let's implement it:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg _ =
    case msg of
        GotJoke result ->
            case result of
                Ok joke ->
                    ( Success joke, Cmd.none )

                Err _ ->
                    ( Failure, Cmd.none )
Enter fullscreen mode Exit fullscreen mode

Here, we pattern match the message - even though we just have a single message. By doing it this way, we'll be able to add features without refactoring too much later.

Inside of the GotJoke match, we're doing another pattern matching case to handle the two different cases of the Result type - an Ok type with the parsed result, and an Err type with the error (which we ignore).

Both the Ok and the Err types return a tuple with the Msg as the first item, and then Cmd.none as the second. Cmd.none indicates that we don't want to trigger a new command as a result of this message. This makes sense in our case - because if we got a result, we're done, and if the API call failed, there's no need to try again immediately.

Implementing a view

I'm a front end programmer after all, so the coolest part for me is always going to be implementing the view. Here's what I put together:

view : Model -> Html Msg
view model =
    div [ A.style "text-align" "center" ]
        [ h1 [] [ text "Elm dad jokes 😎" ]
        , case model of
            Loading ->
                p [] [ text "Loading..." ]

            Failure ->
                p [ A.style "color" "red" ] [ text "Ouch, something went wrong while fetching the stuff over the network" ]

            Success theText ->
                pre [] [ text theText ]
        ]
Enter fullscreen mode Exit fullscreen mode

Here, I create a containing <div />, a heading, and then pattern match our model to create three distinct views based on which state we're in.

Namespacing attributes

Notice the A. in A.style? Previously, I imported all of the possible attributes into the global namespace with import Html.Attributes exposing (..). That turned out to be a bit polluting to my taste. In this app, I'm namespacing them with A, by changing my import to import Html.Attributes as A. This way, I don't pollute the global namespace as much, while still keeping the attribute names short when I need to use them.

Putting it all together

We've forgotten one step - calling the Browser.sandbox function. This time, however, since we're introducing commands into the mix, we need to call a different function - Browser.element. It looks pretty similar, but requires another function - subscriptions. We're not going to bother with subscriptions this time around, so let's just say we don't have any:

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.none
Enter fullscreen mode Exit fullscreen mode

Now, we got all the pieces to call Browser.element:

main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }
Enter fullscreen mode Exit fullscreen mode

That should be it! Let's run our app to get some laughs:

A screen shot of a bunch of HTML where the joke should be

Argh - I must've missed something - I'm not getting the joke, I'm getting the entire HTML page! This is ridiculous, and not in the way I wanted it to be!

After reading the API docs, you have to specify the Accept: text/plain HTTP header to only get the text. But how do I do that?

Try 2 - Http.request

The original Http.get function I called earlier didn't support sending headers, but after heading (hah 😄) over to the very nice documentation for the elm/http package, I came across another function that provided more flexibility - the request function.

After a bit of trial and error, I ended up with changing my init function to this:

init : () -> ( Model, Cmd Msg )
init _ =
    ( Loading
    , Http.request
        { method = "GET"
        , body = Http.emptyBody
        , headers = [ Http.header "Accept" "text/plain" ]
        , url = "https://icanhazdadjoke.com"
        , expect = Http.expectString GotJoke
        , timeout = Nothing
        , tracker = Nothing
        }
    )
Enter fullscreen mode Exit fullscreen mode

A few more lines of code, but still pretty manageable. After refreshing my page, I got what I had been hoping for:

A screen shot showing the joke "I cut my finger cutting cheese. I know it may be a cheesy story but I feel grate now."

Great success!

Fetching more jokes!

Even though that joke truly is a classic, I soon wanted more. So the next feature I wanted to implement was adding a "get a new joke" button! Let's go through the steps I needed to add this new feature.

First, I added a new type to the Msg type:

type Msg
    = GotJoke (Result Http.Error String)
    | RequestNewJoke
Enter fullscreen mode Exit fullscreen mode

Now my update function is broken, since I no longer handle all cases. Let's fix that:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg _ =
    case msg of
        GotJoke result ->
            case result of
                Ok text ->
                    ( Success text, Cmd.none )

                Err _ ->
                    ( Failure, Cmd.none )

        RequestNewJoke ->
            ( Loading, fetchDadJoke )
Enter fullscreen mode Exit fullscreen mode

If we receive a RequestNewJoke message, we set the state to Loading, and trigger the command fetchDadJoke. But where does that last part come from?

The fetchDadJoke command is refactored out from our init function, and is the code that calls the API:

fetchDadJoke : Cmd Msg
fetchDadJoke =
    Http.request
        { method = "GET"
        , body = Http.emptyBody
        , headers = [ Http.header "Accept" "text/plain" ]
        , url = "https://icanhazdadjoke.com"
        , expect = Http.expectString GotJoke
        , timeout = Nothing
        , tracker = Nothing
        }

init : () -> ( Model, Cmd Msg )
init _ =
    ( Loading
    , fetchDadJoke
    )
Enter fullscreen mode Exit fullscreen mode

Neat, right? I guess I could've refactored out the entire tuple, but it looks pretty nice as is, too.

Finally, we just need to add some buttons to our UI. I decided to add a retry button in case of errors, as well:

view : Model -> Html Msg
view model =
    div [ A.style "text-align" "center" ]
        [ h1 [] [ text "Elm dad jokes 😎" ]
        , case model of
            Loading ->
                p [] [ text "Loading..." ]

            Failure ->
                div []
                    [ p [ A.style "color" "red" ]
                        [ text "Ouch, something went wrong while fetching the stuff over the network"
                        ]
                    , button [ Events.onClick RequestNewJoke, A.type_ "button" ]
                        [ text "Try again" ]
                    ]

            Success theText ->
                div []
                    [ pre [] [ text theText ]
                    , button [ Events.onClick RequestNewJoke, A.type_ "button" ]
                        [ text "Get another joke" ]
                    ]
        ]
Enter fullscreen mode Exit fullscreen mode

And that's it, really! We now have "get a new joke" support as well.

Last thoughts and next moves

I learned a lot building this project, and I hope you did as well with following along. There's quite a few new concepts here, with Result and Cmd and what have you. However, it's coming together quite nicely.

This is the third app I've ever built in Elm, and the syntax is starting to finally feel... "right". I was very skeptical at first, but I think it's growing on me.

Handling HTTP requests was a breeze, but very few APIs just return simple text strings. So next, I want to integrate with a fully fledged JSON API, so I can try my hands on decoding and encoding JSON. I'm sure it'll be a hoot!

You can see the entire code for this app in this gist, and even try it out if you want.

After a bit of struggling, I even made a CodeSandbox of it:

Thanks for following along! If you find any bugs, or have any questions, please feel free to ask in the comments!

Top comments (2)

Collapse
 
josephthecoder profile image
Joseph Stevens

As a new dad myself, I say brilliant!

Collapse
 
selbekk profile image
selbekk

Congrats on the kid! 😻