6.11

Creating a Resource Using POST

The only operation left to cover is creating a new resource by using the POST HTTP method. The name of this method has nothing to do with the name we picked for our resource — post. It’s just a coincidence.

Here’s the plan: we’ll add a link that says Create new post below the Refresh posts button on the list page. When that link is clicked, we’ll take the user to a new page which will contain a form for creating a new post. Modify the view function in Views/List.elm like this:

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick FetchPosts ]
            [ text "Refresh posts" ]
        , br [] []
        , br [] []
        , a [ href "/posts/new" ]
            [ text "Create new post" ]
        , viewPostsOrError model
        ]

Run elm-live from the beginning-elm directory in terminal using the following command if it’s not running already.

$ elm-live PostApp/App.elm --pushstate

Also run json-server from the beginning-elm directory in terminal using the following command if it’s not running already.

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

Now go to the list view (http://localhost:8000/posts) and you should see a link for creating a new post.

Matching Path to a Route

We’ll be using /posts/new as the Create New Post page’s path. Let’s parse that in matchRoute located inside Routing.elm.

matchRoute : Parser (Route -> a) a
matchRoute =
    oneOf
        .
        .
        , map PostRoute (s "posts" </> int)
        , map NewPostRoute (s "posts" </> s "new")
        ]

Once again, we’re leveraging the </> operator to combine multiple parsers. Once the s "posts" </> s "new" parser extracts the /posts/new path from a location, we want to map that path to NewPostRoute. Let’s add that route to the Route type in Types.elm.

type Route
    .
    .
    | NotFoundRoute
    | NewPostRoute

Creating the New Post Page

Create a new file called New.elm in the PostApp/Views directory and add the code below to it.

module Views.New exposing (view)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Types exposing (..)


view : Html Msg
view =
    div []
        [ a [ href "/posts" ] [ text "Back" ]
        , h3 [] [ text "Create New Post" ]
        , newPostForm
        ]


newPostForm : Html Msg
newPostForm =
    Html.form []
        [ div []
            [ text "Title"
            , br [] []
            , input
                [ type_ "text"
                , onInput NewPostTitle
                ]
                []
            ]
        , br [] []
        , div []
            [ text "Author Name"
            , br [] []
            , input
                [ type_ "text"
                , onInput NewAuthorName
                ]
                []
            ]
        , br [] []
        , div []
            [ text "Author URL"
            , br [] []
            , input
                [ type_ "text"
                , onInput NewAuthorUrl
                ]
                []
            ]
        , br [] []
        , div []
            [ button [ onClick CreateNewPost ]
                [ text "Submit" ]
            ]
        ]

The code for the Create New Post page looks very similar to the Edit Post page. It’s worth noting that the view function in Views/New.elm doesn’t take any arguments. All other view functions we’ve seen so far took at least one argument. That’s because Views/New.elm is for creating a brand new post, so we don’t need to pre-populate the form with data stored in our model.

In the Saving the Updated Post section, we passed a post ID as the first argument to the messages generated by text fields.

onInput (UpdateTitle post.id)
onInput (UpdateAuthorName post.id)
onInput (UpdateAuthorUrl post.id)

We don’t need to do that in newPostForm because the post hasn’t been created yet. The only thing that needs to be sent to the server is the user-entered content.

Handling Text Field Messages

Add the text field messages listed in newPostForm to the Msg type in Types.elm.

type Msg
    .
    .
    | PostDeleted (Result Http.Error String)
    | NewPostTitle String
    | NewAuthorName String
    | NewAuthorUrl String

Where should we store the new title, author name, and URL? How about we store them in Model? Add a new field called newPost to it in Types.elm.

type alias Model =
    { posts : WebData (List Post)
    , currentRoute : Route
    , newPost : Post
    }

Let’s initialize the newPost field to an empty post in initialModel located in State.elm.

tempPostId =
    -1


emptyPost : Post
emptyPost =
    Author "" ""
        |> Post tempPostId ""


initialModel : Route -> Model
initialModel route =
    { posts = RemoteData.Loading
    , currentRoute = route
    , newPost = emptyPost
    }

We’re using -1 as the empty post’s ID to indicate that it’s temporary. The real ID will be assigned later when the server actually creates a post. It’s highly unlikely that a real post will have a negative number as its ID. Most servers start with a positive number as an ID and keep incrementing it whenever a new resource is created.

Now we’re ready to handle the text field messages. Add three new branches to the update function in State.elm.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        PostDeleted _ ->
            ...

        NewPostTitle newTitle ->
            let
                updatedNewPost =
                    setTitle newTitle model.newPost
            in
                ( { model | newPost = updatedNewPost }, Cmd.none )

        NewAuthorName newName ->
            let
                updatedNewPost =
                    setAuthorName newName model.newPost
            in
                ( { model | newPost = updatedNewPost }, Cmd.none )

        NewAuthorUrl newUrl ->
            let
                updatedNewPost =
                    setAuthorUrl newUrl model.newPost
            in
                ( { model | newPost = updatedNewPost }, Cmd.none )

We’re reusing the three helper functions we created in the Saving the Updated Post section to update the fields in newPost.

setTitle : String -> Post -> Post
setTitle newTitle post =
    { post | title = newTitle }


setAuthorName : String -> Post -> Post
setAuthorName newName post =
    let
        oldAuthor =
            post.author
    in
        { post | author = { oldAuthor | name = newName } }


setAuthorUrl : String -> Post -> Post
setAuthorUrl newUrl post =
    let
        oldAuthor =
            post.author
    in
        { post | author = { oldAuthor | url = newUrl } }

We can make the code in new branches more compact by extracting the part that updates the model out to a new function. Add a function called updateNewPost right below update in State.elm.

updateNewPost :
    String
    -> (String -> Post -> Post)
    -> Model
    -> ( Model, Cmd Msg )
updateNewPost newValue updateFunction model =
    let
        updatedNewPost =
            updateFunction newValue model.newPost
    in
        ( { model | newPost = updatedNewPost }, Cmd.none )

Now modify the new branches in the update function inside State.elm like this:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        NewPostTitle newTitle ->
            updateNewPost newTitle setTitle model

        NewAuthorName newName ->
            updateNewPost newName setAuthorName model

        NewAuthorUrl newUrl ->
            updateNewPost newUrl setAuthorUrl model

Submitting a New Post to Server

Now that the content from all three text fields is stored in our model, we can focus on submitting that info to the server. When the Submit button on the Create New Post page is clicked, it sends the CreateNewPost message to the Elm Runtime. Let’s add this message to the Msg type in Types.elm.

type Msg
    .
    .
    | NewAuthorUrl String
    | CreateNewPost

Add a new branch to the update function in State.elm to handle CreateNewPost.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        NewAuthorUrl newUrl ->
            ...

        CreateNewPost ->
            ( model, createPostCommand model.newPost )

The code for handling the CreateNewPost message is quite simple. All it does is ask the createPostCommand function to return a command. Let’s add the definition for that function along with two more functions it uses to the bottom of Rest.elm.

createPostCommand : Post -> Cmd Msg
createPostCommand post =
    createPostRequest post
        |> Http.send PostCreated


createPostRequest : Post -> Http.Request Post
createPostRequest post =
    Http.request
        { method = "POST"
        , headers = []
        , url = "http://localhost:5019/posts"
        , body = Http.jsonBody (newPostEncoder post)
        , expect = Http.expectJson postDecoder
        , timeout = Nothing
        , withCredentials = False
        }


newPostEncoder : Post -> Encode.Value
newPostEncoder post =
    Encode.object
        [ ( "title", Encode.string post.title )
        , ( "author", authorEncoder post.author )
        ]

The code above looks very similar to what we used for creating a command that updated a post in the Submitting Updated Post to Server section. Here it is once again for comparison:

updatePostCommand : Post -> Cmd Msg
updatePostCommand post =
    updatePostRequest post
        |> Http.send PostUpdated


updatePostRequest : Post -> Http.Request Post
updatePostRequest post =
    Http.request
        { method = "PATCH"
        , headers = []
        , url = "http://localhost:5019/posts/" ++ (toString post.id)
        , body = Http.jsonBody (postEncoder post)
        , expect = Http.expectJson postDecoder
        , timeout = Nothing
        , withCredentials = False
        }


postEncoder : Post -> Encode.Value
postEncoder post =
    Encode.object
        [ ( "id", Encode.int post.id )
        , ( "title", Encode.string post.title )
        , ( "author", authorEncoder post.author )
        ]

One noteworthy difference is the value assigned to the body field inside the record passed to Http.request. We can’t include the ID of a post in JSON body because the server is responsible for creating that ID for us. The JSON that eventually gets sent to the server for creating a new post will look something like this:

{
  "title": "new-title",
  "author": {
    "name": "new-author-name",
    "url": "https://new-author-url.com"
  }
}

Notice the id field is missing. Whereas the JSON for updating an existing post looks something like this:

{
  "id": 2,
  "title": "http-server (modified)",
  "author": {
    "name": "indexzero",
    "url": "https://github.com/indexzero"
  }
}

The postEncoder function includes id. That’s why we can’t use it in createPostRequest. What we need is a separate encoder called newPostEncoder that only includes title and author.

Http.post

The Http module defines a shorthand function called post for creating, well, a POST request. Here’s how its type signature looks:

post : String -> Body -> Decoder a -> Request a

It takes a URL, body, and a decoder and returns a request. We could have used post instead of the low-level function Http.request in createPostRequest, but I find the implementation using post slightly less readable. Here is how createPostRequest would look if we were to use the Http.post function:

createPostUrl : String
createPostUrl =
    "http://localhost:5019/posts"


createPostRequest : Post -> Http.Request Post
createPostRequest post =
    Http.post createPostUrl (Http.jsonBody (newPostEncoder post)) postDecoder

If you prefer post, feel free to use it. We’ll stick with Http.request in this book.

Adding Newly Created Post to model.posts

We haven’t defined the PostCreated message used in createPostCommand yet. Let’s do that by adding it to the Msg type in Types.elm.

type Msg
    .
    .
    | CreateNewPost
    | PostCreated (Result Http.Error Post)

Add two new branches to update in State.elm for handling the PostCreated message.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        CreateNewPost ->
            ...

        PostCreated (Ok post) ->
            ( { model
                | posts = addNewPost post model.posts
                , newPost = emptyPost
              }
            , Cmd.none
            )

        PostCreated (Err _) ->
            ( model, Cmd.none )

The payload inside PostCreated is of Result type. That’s why we added two separate branches for handling the Ok and Err values. For simplicity, the PostCreated (Err _) -> branch returns the model without modifying it. Properly handling errors is left as an exercise. If you don’t remember how to deal with errors, please refer to the Handling HTTP Errors section.

The PostCreated (Ok post) -> branch is slightly more complex. It adds the new post returned by the server to the existing list of posts and also sets the model.newPost field to emptyPost. Let’s implement the addNewPost function right below update in State.elm.

addNewPost : Post -> WebData (List Post) -> WebData (List Post)
addNewPost newPost posts =
    let
        appendPost : List Post -> List Post
        appendPost listOfPosts =
            List.append listOfPosts [ newPost ]
    in
        RemoteData.map appendPost posts

The model.posts field contains a value of type WebData. Once again, here is how that type looks:

type alias WebData a =
    RemoteData Http.Error a


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

As explained in the Saving the Updated Post section, the RemoteData.map function applies the given function to the Success value stored in posts. If the value is anything other than Success, it ignores the function.

Making createPostCommand Visible

Right now the createPostCommand function isn’t visible from outside the Rest module. We need to expose it in the module declaration in Rest.elm.

module Rest
    exposing
        ( fetchPostsCommand
        , updatePostCommand
        , deletePostCommand
        , createPostCommand
        )
.
.

We also need to expose it in State.elm when importing the Rest module.

module State exposing (..)
.
.
import Rest
    exposing
        ( fetchPostsCommand
        , updatePostCommand
        , deletePostCommand
        , createPostCommand
        )
.
.

Routing Users to the Create New Post Page

The only thing remaining is to add a branch to the view function in View.elm for routing users to the Create New Post page.

view : Model -> Html Msg
view model =
    case model.currentRoute of
        .
        .
        NotFoundRoute ->
            ...

        NewPostRoute ->
            Views.New.view

We also need to import the Views.New module in View.elm.

module View exposing (..)
.
.
import Misc exposing (..)
import Views.New

Now we are ready to test. Check the elm-live window in terminal to make sure there are no errors. After that go to the list view (http://localhost:8000/posts) and click Create new post. You’ll be taken to a new page for creating a post. Enter the following info into the text fields and click the Submit button.

If everything goes well, json-server will create a new post. You can verify whether or not that happened by going back to the list view.

The server incremented the ID by 1 to 3 and assigned it to the new post. Originally, we had two posts. We then deleted the first post in the Deleting a Resource Using DELETE section. Usually, the server doesn’t reuse the deleted post’s ID. You can also verify the creation of a new post by checking the server/db.json file.

{
  "posts": [
    .
    .
    {
      "title": "elm-live",
      "author": {
        "name": "architectcodes",
        "url": "https://github.com/architectcodes"
      },
      "id": 3
    }
    .
    .
}

Summary

In this section, we learned how to create a new resource on a server using the POST HTTP method. The process of creating a new resource is very similar to the process of updating an existing resource. One major difference is that we can’t include the ID of that resource in JSON body.

Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required
Close