6.7

Restructuring Code

For the remainder of this chapter, we’ll be exploring how to send the POST, PATCH, and DELETE HTTP requests to create, update, and delete posts respectively. Before we do that though, we need to restructure the code in DecodingJson.elm. It has reached a point where breaking it into smaller modules will make adding new features easier.

The recommended way to manage data flow in an Elm app is to use the Elm Architecture. Elm doesn’t have such a recommendation when it comes to organizing code, so we can restructure our code however we want. There are many different ways to do it, but we’ll follow what Kris Jenkins — the author of RemoteDatarecommends.

Step 1: Creating a New Directory

Let’s create a new directory called PostApp in the beginning-elm directory instead of cluttering elm-examples with too many modules.

We need to add PostApp to the source-directories list in elm-package.json.

{
    .
    .
    "source-directories": [
        ".",
        "elm-examples",
        "PostApp"
    ],
    .
    .
}

Step 2: Extracting Types

We’ll be storing all types we’ve defined so far using either the type or type alias keywords in a file named Types.elm. Create that file inside PostApp and add the following code to it.

module Types exposing (..)

import RemoteData exposing (WebData)


type alias Author =
    { name : String
    , url : String
    }


type alias Post =
    { id : Int
    , title : String
    , author : Author
    }


type alias Model =
    { posts : WebData (List Post)
    }


type Msg
    = FetchPosts
    | PostsReceived (WebData (List Post))

Step 3: Extracting init and update

The init and update functions manage the state of our app, so let’s extract them into a file named State.elm inside PostApp directory.

module State exposing (..)

import RemoteData exposing (WebData)
import Types exposing (..)
import Rest exposing (fetchPostsCommand)


init : ( Model, Cmd Msg )
init =
    ( { posts = RemoteData.Loading }, fetchPostsCommand )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FetchPosts ->
            ( { model | posts = RemoteData.Loading }, fetchPostsCommand )

        PostsReceived response ->
            ( { model | posts = response }, Cmd.none )

In addition to RemoteData and Types, we also imported a non-existent module called Rest. We’ll create that module next.

Step 4: Extracting Commands and Decoders

The command we created contains an HTTP request for fetching a resource from a RESTful API server. Let’s extract that command and the decoders it uses into a file named Rest.elm inside the PostApp directory.

module Rest exposing (fetchPostsCommand)

import Http
import RemoteData
import Types exposing (..)
import Json.Decode exposing (string, int, list, Decoder)
import Json.Decode.Pipeline exposing (decode, required)


authorDecoder : Decoder Author
authorDecoder =
    decode Author
        |> required "name" string
        |> required "url" string


postDecoder : Decoder Post
postDecoder =
    decode Post
        |> required "id" int
        |> required "title" string
        |> required "author" authorDecoder


fetchPostsCommand : Cmd Msg
fetchPostsCommand =
    list postDecoder
        |> Http.get "http://localhost:5019/posts"
        |> RemoteData.sendRequest
        |> Cmd.map PostsReceived

It’s worth noticing that we’re only exposing the fetchPostsCommand from the Rest module. We don’t want to expose the decoders because they aren’t used anywhere outside of this module.

Step 5: Extracting Main

Since main is the entry point for our app, we’ll extract it into a file called App.elm. Create that file in the PostApp directory and add the following code to it.

module App exposing (main)

import Html exposing (program)
import State exposing (init, update)
import Views.List exposing (view)
import Types exposing (..)


main : Program Never Model Msg
main =
    program
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }

We imported a non-existent module called Views.List. Let’s create that next.

Step 6: Extracting View Code

The only thing remaining to extract out of DecodingJson.elm is the view code. Right now we have only one page that displays the posts. Soon we will be creating additional pages for updating and creating posts, so let’s create a new directory called Views inside PostApp.

Next create a file named List.elm inside the Views directory and add the following code to it.

module Views.List exposing (view)

import Types exposing (..)
import Html exposing (..)
import Html.Attributes exposing (href)
import Html.Events exposing (onClick)
import Http
import RemoteData


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick FetchPosts ]
            [ text "Refresh posts" ]
        , viewPostsOrError model
        ]


viewPostsOrError : Model -> Html Msg
viewPostsOrError model =
    case model.posts of
        RemoteData.NotAsked ->
            text ""

        RemoteData.Loading ->
            h3 [] [ text "Loading..." ]

        RemoteData.Success posts ->
            viewPosts posts

        RemoteData.Failure httpError ->
            viewError (createErrorMessage httpError)


viewError : String -> Html Msg
viewError errorMessage =
    let
        errorHeading =
            "Couldn't fetch data at this time."
    in
        div []
            [ h3 [] [ text errorHeading ]
            , text ("Error: " ++ errorMessage)
            ]


viewPosts : List Post -> Html Msg
viewPosts posts =
    div []
        [ h3 [] [ text "Posts" ]
        , table []
            ([ viewTableHeader ] ++ List.map viewPost posts)
        ]


viewTableHeader : Html Msg
viewTableHeader =
    tr []
        [ th []
            [ text "ID" ]
        , th []
            [ text "Title" ]
        , th []
            [ text "Author" ]
        ]


viewPost : Post -> Html Msg
viewPost post =
    tr []
        [ td []
            [ text (toString post.id) ]
        , td []
            [ text post.title ]
        , td []
            [ a [ href post.author.url ] [ text post.author.name ] ]
        ]


createErrorMessage : Http.Error -> String
createErrorMessage httpError =
    case httpError of
        Http.BadUrl message ->
            message

        Http.Timeout ->
            "Server is taking too long to respond. Please try again later."

        Http.NetworkError ->
            "It appears you don't have an Internet connection right now."

        Http.BadStatus response ->
            response.status.message

        Http.BadPayload message response ->
            message

We have to use a dot in module’s name because List.elm is inside the View directory. If we remove the dot and use ViewsList instead, the compiler will throw an error.

The dot tells the compiler that this module is included in the file named List.elm which is inside the directory Views, so go look there. If you don’t want to use ., you can still make it work by renaming the file to ViewsList.elm and adding the PostApp/Views entry to elm-package.json.

{
    .
    .
    "source-directories": [
        ".",
        "elm-examples",
        "PostApp",
        "PostApp/Views"
    ],
    .
    .
}

Not only does that take more work, it also makes the file and module names not as elegant compared to the previous approach. Therefore, we will continue to use the dot in a module’s name if that module is inside the Views directory.

Step 7: Verify Everything is Working

We’re done with refactoring. Let’s make sure everything is working as expected. Run json-server with a delay from the beginning-elm directory in terminal if it’s not already running.

$ json-server --watch server/db.json -p 5019 --delay 2000

Also run elm-reactor from the beginning-elm directory in terminal and go to this URL in a browser: http://localhost:8000. Click PostApp and then App.elm to run the app. You should see the loading text for two seconds and then a list of posts.

Summary

It’s easier to start building an Elm app with a single file. A time will come when the code in that file starts to grow to a point where finding code becomes harder. That’s when you should consider extracting related parts out to separate modules as we did in this section. In the next section, we’ll create a new page for editing a post and learn how to route users to that page. Here’s how the contents of PostApp directory should look thus far:

Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required
Close