# Lesson 6: HTTP & JSON ## Learning Goals By the end of this lesson, you will: - Understand Commands (Cmd) in Elm - Make HTTP requests - Decode JSON responses - Handle loading states and errors - Understand how Elm manages side effects ## Side Effects in Elm Remember: Elm functions are **pure** - they can't do side effects directly. So how do we: - Make HTTP requests? - Generate random numbers? - Get the current time? - Write to local storage? Answer: **Commands (Cmd)** ### The Command Pattern Instead of performing side effects, you **describe** them: ```elm -- Instead of actually making a request: -- response = http.get("https://api.example.com") -- NOT how Elm works -- You return a command describing what you want: update msg model = case msg of FetchData -> ( model, Http.get { url = "...", expect = ... } ) -- Returns the model AND a command ``` Elm runtime executes the command and sends the result back as a message. ## Browser.element: Elm with Commands We need to upgrade from `Browser.sandbox` to `Browser.element`: ```elm main = Browser.element { init = init , update = update , view = view , subscriptions = subscriptions } ``` ### Key Differences | Browser.sandbox | Browser.element | |-----------------|-----------------| | `init : Model` | `init : flags -> (Model, Cmd Msg)` | | `update : Msg -> Model -> Model` | `update : Msg -> Model -> (Model, Cmd Msg)` | | No side effects | Can perform side effects via Cmd | ## Your First HTTP Request Let's fetch a random quote from an API. ### Step 1: Set Up the Project ```bash mkdir http-example cd http-example elm init elm install elm/http elm install elm/json ``` ### Step 2: The Complete Example ```elm module Main exposing (main) import Browser import Html exposing (Html, button, div, p, text) import Html.Events exposing (onClick) import Http -- MODEL type Model = Loading | Success String | Failure String init : () -> ( Model, Cmd Msg ) init _ = ( Loading, fetchQuote ) -- UPDATE type Msg = GotQuote (Result Http.Error String) | FetchNewQuote update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of GotQuote result -> case result of Ok quote -> ( Success quote, Cmd.none ) Err error -> ( Failure (errorToString error), Cmd.none ) FetchNewQuote -> ( Loading, fetchQuote ) errorToString : Http.Error -> String errorToString error = case error of Http.BadUrl url -> "Bad URL: " ++ url Http.Timeout -> "Request timed out" Http.NetworkError -> "Network error" Http.BadStatus status -> "Bad status: " ++ String.fromInt status Http.BadBody message -> "Bad body: " ++ message -- HTTP fetchQuote : Cmd Msg fetchQuote = Http.get { url = "https://api.quotable.io/random" , expect = Http.expectJson GotQuote quoteDecoder } quoteDecoder : Json.Decode.Decoder String quoteDecoder = Json.Decode.field "content" Json.Decode.string -- VIEW view : Model -> Html Msg view model = div [] [ case model of Loading -> text "Loading..." Success quote -> div [] [ p [] [ text quote ] , button [ onClick FetchNewQuote ] [ text "New Quote" ] ] Failure error -> div [] [ p [] [ text ("Error: " ++ error) ] , button [ onClick FetchNewQuote ] [ text "Try Again" ] ] ] -- SUBSCRIPTIONS subscriptions : Model -> Sub Msg subscriptions _ = Sub.none -- MAIN main = Browser.element { init = init , update = update , view = view , subscriptions = subscriptions } ``` ### Breaking It Down #### The Model as State Machine ```elm type Model = Loading | Success String | Failure String ``` Using a custom type for the model means we can only be in ONE state at a time. No more: ```javascript // JavaScript { isLoading: false, hasError: true, data: null } // Confusing! ``` #### Init Returns a Command ```elm init : () -> ( Model, Cmd Msg ) init _ = ( Loading, fetchQuote ) -- Start in Loading state, fetch immediately ``` The `()` is called "unit" - it means no flags are passed. We return a **tuple** of model and command. #### Update Returns a Command ```elm update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of GotQuote result -> case result of Ok quote -> ( Success quote, Cmd.none ) -- No more commands Err error -> ( Failure (errorToString error), Cmd.none ) ``` `Cmd.none` means "no command to execute." #### The HTTP Request ```elm fetchQuote : Cmd Msg fetchQuote = Http.get { url = "https://api.quotable.io/random" , expect = Http.expectJson GotQuote quoteDecoder } ``` - `Http.get` creates a GET request command - `expect` tells Elm what to do with the response - `GotQuote` is the message constructor that will wrap the result - `quoteDecoder` parses the JSON ## JSON Decoding Elm doesn't just convert JSON to any type automatically. You must define a **decoder** that describes the JSON structure. ### Why Decoders? JavaScript: ```javascript const data = JSON.parse(response); console.log(data.user.name); // Might crash if structure is wrong! ``` Elm: The decoder either succeeds with the exact type you expect, or fails gracefully. ### Basic Decoders ```elm import Json.Decode exposing (Decoder, string, int, float, bool) -- Primitive decoders string : Decoder String -- Decodes "hello" to "hello" int : Decoder Int -- Decodes 42 to 42 float : Decoder Float -- Decodes 3.14 to 3.14 bool : Decoder Bool -- Decodes true to True ``` ### Decoding Objects ```elm import Json.Decode exposing (Decoder, field, string, int) -- JSON: { "name": "Alice" } nameDecoder : Decoder String nameDecoder = field "name" string -- JSON: { "user": { "name": "Alice" } } nestedNameDecoder : Decoder String nestedNameDecoder = field "user" (field "name" string) ``` ### Decoding Multiple Fields ```elm import Json.Decode exposing (Decoder, map2, field, string, int) type alias User = { name : String , age : Int } -- JSON: { "name": "Alice", "age": 30 } userDecoder : Decoder User userDecoder = map2 User (field "name" string) (field "age" int) ``` `map2` takes: 1. A constructor function (User) 2. Decoder for first field 3. Decoder for second field For more fields, use `map3`, `map4`, etc., or the `elm/json-decode-pipeline` package. ### Using Pipeline Style (Recommended) Install: ```bash elm install NoRedInk/elm-json-decode-pipeline ``` ```elm import Json.Decode exposing (Decoder, string, int) import Json.Decode.Pipeline exposing (required, optional) type alias User = { name : String , age : Int , email : Maybe String } userDecoder : Decoder User userDecoder = Json.Decode.succeed User |> required "name" string |> required "age" int |> optional "email" (Json.Decode.map Just string) Nothing ``` ### Decoding Lists ```elm import Json.Decode exposing (Decoder, list, string) -- JSON: ["apple", "banana", "cherry"] fruitsDecoder : Decoder (List String) fruitsDecoder = list string -- JSON: [{ "name": "Alice" }, { "name": "Bob" }] usersDecoder : Decoder (List User) usersDecoder = list userDecoder ``` ### Decoding with Maybe ```elm -- JSON: { "name": "Alice", "nickname": null } -- Or: { "name": "Alice" } (nickname missing) type alias Person = { name : String , nickname : Maybe String } personDecoder : Decoder Person personDecoder = map2 Person (field "name" string) (Json.Decode.maybe (field "nickname" string)) ``` ### Testing Decoders ```elm import Json.Decode exposing (decodeString) result = decodeString userDecoder """{"name": "Alice", "age": 30}""" -- Ok { name = "Alice", age = 30 } badResult = decodeString userDecoder """{"name": "Alice"}""" -- Err ... (missing field "age") ``` ## POST Requests with JSON Body ```elm import Http import Json.Encode as Encode createUser : String -> Int -> Cmd Msg createUser name age = Http.post { url = "https://api.example.com/users" , body = Http.jsonBody (encodeUser name age) , expect = Http.expectJson GotNewUser userDecoder } encodeUser : String -> Int -> Encode.Value encodeUser name age = Encode.object [ ( "name", Encode.string name ) , ( "age", Encode.int age ) ] ``` ### JSON Encoding ```elm import Json.Encode as Encode -- Primitives Encode.string "hello" -- "hello" Encode.int 42 -- 42 Encode.float 3.14 -- 3.14 Encode.bool True -- true Encode.null -- null -- Objects Encode.object [ ( "name", Encode.string "Alice" ) , ( "age", Encode.int 30 ) ] -- { "name": "Alice", "age": 30 } -- Lists Encode.list Encode.int [1, 2, 3] -- [1, 2, 3] ``` ## Complete Example: GitHub User Search ```elm module GitHubSearch exposing (main) import Browser import Html exposing (Html, button, div, img, input, li, text, ul) import Html.Attributes exposing (placeholder, src, value, width) import Html.Events exposing (onClick, onInput) import Http import Json.Decode exposing (Decoder, field, int, list, string) -- MODEL type alias User = { login : String , avatarUrl : String , id : Int } type Model = Initial | Loading String | Success String (List User) | Failure String String init : () -> ( Model, Cmd Msg ) init _ = ( Initial, Cmd.none ) -- UPDATE type Msg = UpdateSearch String | Search | GotUsers (Result Http.Error (List User)) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of UpdateSearch query -> case model of Initial -> ( Initial, Cmd.none ) Loading q -> ( Loading query, Cmd.none ) Success _ users -> ( Success query users, Cmd.none ) Failure _ error -> ( Failure query error, Cmd.none ) Search -> let query = getQuery model in if String.isEmpty query then ( model, Cmd.none ) else ( Loading query, searchUsers query ) GotUsers result -> let query = getQuery model in case result of Ok users -> ( Success query users, Cmd.none ) Err error -> ( Failure query (errorToString error), Cmd.none ) getQuery : Model -> String getQuery model = case model of Initial -> "" Loading q -> q Success q _ -> q Failure q _ -> q errorToString : Http.Error -> String errorToString error = case error of Http.BadUrl url -> "Bad URL: " ++ url Http.Timeout -> "Request timed out" Http.NetworkError -> "Network error" Http.BadStatus status -> "Bad status: " ++ String.fromInt status Http.BadBody message -> "Bad body: " ++ message -- HTTP searchUsers : String -> Cmd Msg searchUsers query = Http.get { url = "https://api.github.com/search/users?q=" ++ query , expect = Http.expectJson GotUsers usersDecoder } usersDecoder : Decoder (List User) usersDecoder = field "items" (list userDecoder) userDecoder : Decoder User userDecoder = Json.Decode.map3 User (field "login" string) (field "avatar_url" string) (field "id" int) -- VIEW view : Model -> Html Msg view model = div [] [ div [] [ input [ placeholder "Search GitHub users..." , value (getQuery model) , onInput UpdateSearch ] [] , button [ onClick Search ] [ text "Search" ] ] , viewResults model ] viewResults : Model -> Html Msg viewResults model = case model of Initial -> div [] [ text "Enter a search term" ] Loading _ -> div [] [ text "Searching..." ] Success _ users -> if List.isEmpty users then div [] [ text "No users found" ] else ul [] (List.map viewUser users) Failure _ error -> div [] [ text ("Error: " ++ error) ] viewUser : User -> Html Msg viewUser user = li [] [ img [ src user.avatarUrl, width 50 ] [] , text (" " ++ user.login) ] -- SUBSCRIPTIONS subscriptions : Model -> Sub Msg subscriptions _ = Sub.none -- MAIN main = Browser.element { init = init , update = update , view = view , subscriptions = subscriptions } ``` ## Exercise 6.1: Basic HTTP Request Create an app that: 1. Fetches a random joke from `https://official-joke-api.appspot.com/random_joke` 2. Displays the setup and punchline 3. Has a "New Joke" button The API returns: ```json { "type": "general", "setup": "Why did the chicken...", "punchline": "To get to the other side!", "id": 123 } ```
Solution ```elm module JokeApp exposing (main) import Browser import Html exposing (Html, button, div, p, text) import Html.Events exposing (onClick) import Http import Json.Decode exposing (Decoder, field, string) type alias Joke = { setup : String , punchline : String } type Model = Loading | Success Joke | Failure String init : () -> ( Model, Cmd Msg ) init _ = ( Loading, fetchJoke ) type Msg = GotJoke (Result Http.Error Joke) | FetchNewJoke update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of GotJoke result -> case result of Ok joke -> ( Success joke, Cmd.none ) Err _ -> ( Failure "Failed to fetch joke", Cmd.none ) FetchNewJoke -> ( Loading, fetchJoke ) fetchJoke : Cmd Msg fetchJoke = Http.get { url = "https://official-joke-api.appspot.com/random_joke" , expect = Http.expectJson GotJoke jokeDecoder } jokeDecoder : Decoder Joke jokeDecoder = Json.Decode.map2 Joke (field "setup" string) (field "punchline" string) view : Model -> Html Msg view model = div [] [ case model of Loading -> text "Loading..." Success joke -> div [] [ p [] [ text joke.setup ] , p [] [ text joke.punchline ] , button [ onClick FetchNewJoke ] [ text "New Joke" ] ] Failure error -> div [] [ text error , button [ onClick FetchNewJoke ] [ text "Try Again" ] ] ] subscriptions : Model -> Sub Msg subscriptions _ = Sub.none main = Browser.element { init = init , update = update , view = view , subscriptions = subscriptions } ```
## Exercise 6.2: Nested JSON Decoding Write a decoder for this JSON: ```json { "user": { "profile": { "name": "Alice", "bio": "Elm enthusiast" }, "stats": { "followers": 100, "following": 50 } } } ``` Into this type: ```elm type alias UserInfo = { name : String , bio : String , followers : Int , following : Int } ```
Solution ```elm import Json.Decode exposing (Decoder, field, int, map4, string) userInfoDecoder : Decoder UserInfo userInfoDecoder = map4 UserInfo (field "user" (field "profile" (field "name" string))) (field "user" (field "profile" (field "bio" string))) (field "user" (field "stats" (field "followers" int))) (field "user" (field "stats" (field "following" int))) -- Or using Json.Decode.at for cleaner nested access: userInfoDecoder2 : Decoder UserInfo userInfoDecoder2 = map4 UserInfo (Json.Decode.at [ "user", "profile", "name" ] string) (Json.Decode.at [ "user", "profile", "bio" ] string) (Json.Decode.at [ "user", "stats", "followers" ] int) (Json.Decode.at [ "user", "stats", "following" ] int) ```
## Key Takeaways 1. **Commands describe side effects** - Elm runtime performs them 2. **HTTP requests return Results** - Success or failure 3. **JSON decoders are type-safe** - Compiler ensures correct parsing 4. **Model states** - Use custom types to represent Loading/Success/Failure 5. **Update returns (Model, Cmd)** - New state plus commands to run ## What's Next? In [Lesson 7](07-final-project.md), we'll build a complete application: - A todo list with persistence - Multiple pages - All the concepts combined! --- [← Previous: Lesson 5](05-lists-maybe.md) | [Next: Lesson 7 - Final Project →](07-final-project.md)