Claude Code Plugins

Community-maintained marketplace

Feedback

elm-development

@manutej/fstar-labs
0
0

Comprehensive Elm functional programming for web development including The Elm Architecture, type system, JavaScript interop, and production-ready patterns

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name elm-development
description Comprehensive Elm functional programming for web development including The Elm Architecture, type system, JavaScript interop, and production-ready patterns

Elm Development

A comprehensive skill for building reliable web applications using Elm, a functional programming language that compiles to JavaScript. Elm offers no runtime errors in practice, friendly compiler messages, and a delightful development experience through strong typing and pure functional programming.

When to Use This Skill

Use this skill when:

  • Building web applications that require high reliability and zero runtime errors
  • Creating SPAs (Single Page Applications) with predictable state management
  • Developing user interfaces with strong type safety guarantees
  • Working on projects where refactoring safety is critical
  • Building applications with complex state that needs to be managed cleanly
  • Creating web apps that benefit from functional programming patterns
  • Developing projects where friendly error messages aid rapid development
  • Integrating Elm components into existing JavaScript applications
  • Building interactive web applications with real-time features
  • Creating maintainable, long-lived codebases with clear contracts

Core Concepts

The Elm Architecture (TEA)

The Elm Architecture is a pattern for building interactive applications that emerged naturally from Elm development. It inspired Redux and similar state management patterns.

Core Components:

  1. Model: The complete state of your application
  2. View: A pure function that renders the model as HTML
  3. Update: A pure function that updates the model based on messages

Data Flow:

Model → View → HTML
   ↑             ↓
   └── Update ←──┘
        Messages

Basic Pattern:

type alias Model =
    { count : Int }

type Msg
    = Increment
    | Decrement

init : Model
init =
    { count = 0 }

update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Decrement ] [ text "-" ]
        , div [] [ text (String.fromInt model.count) ]
        , button [ onClick Increment ] [ text "+" ]
        ]

Extended Architecture with Effects

For real applications, you need side effects (HTTP, random, etc.):

Enhanced Components:

  • init: Returns (Model, Cmd Msg) - initial state plus commands to run
  • update: Returns (Model, Cmd Msg) - new state plus commands to run
  • subscriptions: Listens to external events (time, websockets, etc.)
  • view: Same pure function rendering HTML

Program Types:

  • Browser.sandbox: Simple apps without side effects
  • Browser.element: Apps with side effects (HTTP, etc.)
  • Browser.document: Control over <title> and <body>
  • Browser.application: Full SPA with URL routing

Type System Fundamentals

Elm's type system prevents runtime errors through static analysis and type inference.

Type Annotations:

-- Function type annotations
add : Int -> Int -> Int
add x y = x + y

-- Record type alias
type alias User =
    { id : Int
    , name : String
    , email : String
    }

-- Custom types (sum types / tagged unions)
type UserStatus
    = Active
    | Suspended
    | Deleted

-- Parametric types
type Result error value
    = Ok value
    | Err error

Type Inference: Elm infers types automatically, but annotations are recommended for top-level functions:

-- Compiler infers: String -> String
greet name =
    "Hello, " ++ name

-- Better: Add explicit annotation
greet : String -> String
greet name =
    "Hello, " ++ name

Benefits:

  • Catches type errors at compile time
  • Provides excellent error messages
  • Enables fearless refactoring
  • Documents function signatures
  • No null/undefined errors

Maybe and Result Types

Elm eliminates null/undefined errors using Maybe and Result.

Maybe Type:

type Maybe a
    = Just a
    | Nothing

-- Example: Finding a user
findUser : Int -> Maybe User
findUser id =
    -- Returns Just user if found, Nothing otherwise
    ...

-- Pattern matching on Maybe
case findUser 123 of
    Just user ->
        "Found: " ++ user.name

    Nothing ->
        "User not found"

-- Helper functions
Maybe.withDefault : a -> Maybe a -> a
Maybe.map : (a -> b) -> Maybe a -> Maybe b
Maybe.andThen : (a -> Maybe b) -> Maybe a -> Maybe b

Result Type:

type Result error value
    = Ok value
    | Err error

-- Example: Validating user input
validateAge : String -> Result String Int
validateAge input =
    case String.toInt input of
        Nothing ->
            Err "Not a valid number"

        Just age ->
            if age < 0 then
                Err "Age cannot be negative"
            else if age > 150 then
                Err "Age seems unrealistic"
            else
                Ok age

-- Using Result
case validateAge "25" of
    Ok age ->
        "Valid age: " ++ String.fromInt age

    Err error ->
        "Error: " ++ error

Pattern Matching

Pattern matching is fundamental to Elm programming:

-- Matching on custom types
type Traffic
    = Red
    | Yellow
    | Green

describe : Traffic -> String
describe light =
    case light of
        Red ->
            "Stop"
        Yellow ->
            "Prepare to stop"
        Green ->
            "Go"

-- Matching on lists
describeList : List a -> String
describeList list =
    case list of
        [] ->
            "Empty list"

        [single] ->
            "One item"

        [first, second] ->
            "Two items"

        _ ->
            "Many items"

-- Matching with extraction
type User
    = Anonymous
    | LoggedIn String Int

getUserName : User -> String
getUserName user =
    case user of
        Anonymous ->
            "Guest"

        LoggedIn name _ ->
            name

Language Features

Functions

Function Definition:

-- Simple function
double : Int -> Int
double x =
    x * 2

-- Multiple parameters
add : Int -> Int -> Int
add x y =
    x + y

-- Partial application
add5 : Int -> Int
add5 =
    add 5

-- Anonymous functions (lambdas)
doubleList : List Int -> List Int
doubleList numbers =
    List.map (\n -> n * 2) numbers

-- Pipe operator
result : Int
result =
    [1, 2, 3, 4, 5]
        |> List.map double
        |> List.filter (\n -> n > 5)
        |> List.sum

-- Function composition
addThenDouble : Int -> Int
addThenDouble =
    add 3 >> double

Let Expressions:

calculateArea : Float -> Float -> Float
calculateArea width height =
    let
        perimeter =
            2 * (width + height)

        area =
            width * height
    in
    area

Records

Record Syntax:

-- Record type alias
type alias Point =
    { x : Float
    , y : Float
    }

-- Creating records
origin : Point
origin =
    { x = 0, y = 0 }

-- Accessing fields
getX : Point -> Float
getX point =
    point.x

-- Field access function
.x origin  -- Returns 0

-- Updating records (immutable)
moveRight : Point -> Point
moveRight point =
    { point | x = point.x + 1 }

-- Updating multiple fields
moveDiagonal : Point -> Point
moveDiagonal point =
    { point
        | x = point.x + 1
        , y = point.y + 1
    }

-- Pattern matching on records
distance : Point -> Float
distance { x, y } =
    sqrt (x * x + y * y)

Lists and Arrays

Lists (Linked Lists):

-- List literals
numbers : List Int
numbers =
    [1, 2, 3, 4, 5]

-- Cons operator
moreNumbers : List Int
moreNumbers =
    0 :: numbers  -- [0, 1, 2, 3, 4, 5]

-- List functions
List.map : (a -> b) -> List a -> List b
List.filter : (a -> Bool) -> List a -> List a
List.foldl : (a -> b -> b) -> b -> List a -> b
List.length : List a -> Int
List.reverse : List a -> List a
List.sort : List comparable -> List comparable
List.concat : List (List a) -> List a

-- Example usage
processNumbers : List Int -> Int
processNumbers nums =
    nums
        |> List.filter (\n -> n > 0)
        |> List.map (\n -> n * 2)
        |> List.sum

Arrays (Random Access):

import Array exposing (Array)

-- Arrays for fast random access
numbers : Array Int
numbers =
    Array.fromList [1, 2, 3, 4, 5]

-- Array operations
Array.get : Int -> Array a -> Maybe a
Array.set : Int -> a -> Array a -> Array a
Array.push : a -> Array a -> Array a
Array.length : Array a -> Int

Modules and Imports

Module Definition:

module Utils exposing (capitalize, truncate)

-- Private function (not exposed)
isBlank : String -> Bool
isBlank str =
    String.trim str == ""

-- Public function
capitalize : String -> String
capitalize str =
    if isBlank str then
        str
    else
        String.toUpper (String.left 1 str) ++ String.dropLeft 1 str

-- Public function
truncate : Int -> String -> String
truncate maxLength str =
    if String.length str > maxLength then
        String.left (maxLength - 3) str ++ "..."
    else
        str

Import Syntax:

-- Import entire module
import List
import Dict

-- Import with alias
import Json.Decode as Decode
import Json.Encode as Encode

-- Import specific functions
import List exposing (map, filter, foldl)

-- Import all exposed functions (use sparingly)
import Html exposing (..)

The Elm Architecture in Depth

Commands (Cmd)

Commands represent side effects to perform:

type Msg
    = FetchUser
    | GotUser (Result Http.Error User)

init : (Model, Cmd Msg)
init =
    ( { user = Nothing }
    , Cmd.none  -- No command initially
    )

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        FetchUser ->
            ( model
            , Http.get
                { url = "/api/user"
                , expect = Http.expectJson GotUser userDecoder
                }
            )

        GotUser result ->
            case result of
                Ok user ->
                    ( { model | user = Just user }
                    , Cmd.none
                    )

                Err _ ->
                    ( model
                    , Cmd.none
                    )

-- Combining commands
Cmd.batch : List (Cmd msg) -> Cmd msg
Cmd.batch
    [ fetchUser
    , fetchPosts
    , logEvent
    ]

Subscriptions (Sub)

Subscriptions listen to external events:

import Browser.Events
import Time

type Msg
    = Tick Time.Posix
    | KeyPressed String
    | MouseMoved Int Int

subscriptions : Model -> Sub Msg
subscriptions model =
    if model.isActive then
        Sub.batch
            [ Time.every 1000 Tick
            , Browser.Events.onKeyPress keyDecoder
            , Browser.Events.onMouseMove mouseDecoder
            ]
    else
        Sub.none

-- Subscription functions
Time.every : Float -> (Time.Posix -> msg) -> Sub msg
Browser.Events.onKeyPress : Decode.Decoder msg -> Sub msg
Browser.Events.onAnimationFrame : (Time.Posix -> msg) -> Sub msg

View Functions

HTML Generation:

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

view : Model -> Html Msg
view model =
    div [ class "container" ]
        [ header [ class "header" ]
            [ h1 [] [ text "My App" ]
            ]
        , main_ [ class "content" ]
            [ viewUserForm model.form
            , viewUserList model.users
            ]
        , footer [ class "footer" ]
            [ text "© 2025" ]
            ]

-- Conditional rendering
viewIf : Bool -> Html msg -> Html msg
viewIf condition content =
    if condition then
        content
    else
        text ""

-- List rendering
viewUserList : List User -> Html Msg
viewUserList users =
    div [ class "user-list" ]
        [ h2 [] [ text "Users" ]
        , ul [] (List.map viewUser users)
        ]

viewUser : User -> Html Msg
viewUser user =
    li [ class "user-item" ]
        [ text user.name
        , button [ onClick (DeleteUser user.id) ]
            [ text "Delete" ]
        ]

State Management Patterns

Nested Updates:

type alias Model =
    { loginForm : LoginForm
    , settings : Settings
    }

type Msg
    = LoginFormMsg LoginFormMsg
    | SettingsMsg SettingsMsg

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        LoginFormMsg formMsg ->
            let
                (newForm, formCmd) =
                    LoginForm.update formMsg model.loginForm
            in
            ( { model | loginForm = newForm }
            , Cmd.map LoginFormMsg formCmd
            )

        SettingsMsg settingsMsg ->
            let
                (newSettings, settingsCmd) =
                    Settings.update settingsMsg model.settings
            in
            ( { model | settings = newSettings }
            , Cmd.map SettingsMsg settingsCmd
            )

HTTP and JSON

Making HTTP Requests

GET Requests:

import Http
import Json.Decode as Decode

type Msg
    = FetchData
    | GotData (Result Http.Error String)

fetchData : Cmd Msg
fetchData =
    Http.get
        { url = "https://api.example.com/data"
        , expect = Http.expectString GotData
        }

-- With JSON decoder
type alias User =
    { id : Int
    , name : String
    }

fetchUser : Int -> Cmd Msg
fetchUser userId =
    Http.get
        { url = "https://api.example.com/users/" ++ String.fromInt userId
        , expect = Http.expectJson GotUser userDecoder
        }

userDecoder : Decode.Decoder User
userDecoder =
    Decode.map2 User
        (Decode.field "id" Decode.int)
        (Decode.field "name" Decode.string)

POST Requests:

import Json.Encode as Encode

type Msg
    = CreateUser
    | UserCreated (Result Http.Error User)

createUser : String -> String -> Cmd Msg
createUser name email =
    Http.post
        { url = "https://api.example.com/users"
        , body = Http.jsonBody (encodeUser name email)
        , expect = Http.expectJson UserCreated userDecoder
        }

encodeUser : String -> String -> Encode.Value
encodeUser name email =
    Encode.object
        [ ("name", Encode.string name)
        , ("email", Encode.string email)
        ]

Custom Requests:

updateUser : Int -> User -> Cmd Msg
updateUser userId user =
    Http.request
        { method = "PUT"
        , headers = [ Http.header "Authorization" "Bearer token123" ]
        , url = "https://api.example.com/users/" ++ String.fromInt userId
        , body = Http.jsonBody (encodeUserUpdate user)
        , expect = Http.expectJson UserUpdated userDecoder
        , timeout = Just 10000
        , tracker = Nothing
        }

JSON Decoding

Basic Decoders:

import Json.Decode as Decode exposing (Decoder)

-- Primitive decoders
stringDecoder : Decoder String
stringDecoder = Decode.string

intDecoder : Decoder Int
intDecoder = Decode.int

floatDecoder : Decoder Float
floatDecoder = Decode.float

boolDecoder : Decoder Bool
boolDecoder = Decode.bool

-- Field extraction
nameDecoder : Decoder String
nameDecoder =
    Decode.field "name" Decode.string

-- Nested fields
streetDecoder : Decoder String
streetDecoder =
    Decode.at ["address", "street"] Decode.string

Complex Decoders:

type alias User =
    { id : Int
    , name : String
    , email : String
    , age : Maybe Int
    , isActive : Bool
    }

userDecoder : Decoder User
userDecoder =
    Decode.map5 User
        (Decode.field "id" Decode.int)
        (Decode.field "name" Decode.string)
        (Decode.field "email" Decode.string)
        (Decode.maybe (Decode.field "age" Decode.int))
        (Decode.field "is_active" Decode.bool)

-- Alternative with pipeline style (requires elm-json-decode-pipeline)
userDecoderPipeline : Decoder User
userDecoderPipeline =
    Decode.succeed User
        |> required "id" Decode.int
        |> required "name" Decode.string
        |> required "email" Decode.string
        |> optional "age" (Decode.maybe Decode.int) Nothing
        |> required "is_active" Decode.bool

List and Array Decoders:

-- Decode list of users
usersDecoder : Decoder (List User)
usersDecoder =
    Decode.list userDecoder

-- Decode field containing list
postsDecoder : Decoder (List Post)
postsDecoder =
    Decode.field "posts" (Decode.list postDecoder)

-- Decode dictionary
userByIdDecoder : Decoder (Dict String User)
userByIdDecoder =
    Decode.dict userDecoder

Handling Decode Errors:

type Msg
    = GotData (Result Http.Error Data)

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        GotData result ->
            case result of
                Ok data ->
                    ( { model | data = Just data }
                    , Cmd.none
                    )

                Err httpError ->
                    case httpError of
                        Http.BadUrl url ->
                            ( { model | error = Just ("Bad URL: " ++ url) }
                            , Cmd.none
                            )

                        Http.Timeout ->
                            ( { model | error = Just "Request timeout" }
                            , Cmd.none
                            )

                        Http.NetworkError ->
                            ( { model | error = Just "Network error" }
                            , Cmd.none
                            )

                        Http.BadStatus code ->
                            ( { model | error = Just ("Bad status: " ++ String.fromInt code) }
                            , Cmd.none
                            )

                        Http.BadBody message ->
                            ( { model | error = Just ("Decode error: " ++ message) }
                            , Cmd.none
                            )

JavaScript Interop

Flags (Initial Data)

Pass data from JavaScript to Elm on initialization:

Elm Side:

type alias Flags =
    { apiKey : String
    , userId : Int
    , theme : String
    }

init : Flags -> (Model, Cmd Msg)
init flags =
    ( { apiKey = flags.apiKey
      , userId = flags.userId
      , theme = flags.theme
      , data = Nothing
      }
    , fetchUserData flags.userId
    )

main : Program Flags Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

JavaScript Side:

var app = Elm.Main.init({
    node: document.getElementById('app'),
    flags: {
        apiKey: 'your-api-key',
        userId: 12345,
        theme: 'dark'
    }
});

Ports (Bidirectional Communication)

Defining Ports:

port module Main exposing (..)

-- Port for sending data to JavaScript
port saveToLocalStorage : String -> Cmd msg

-- Port for receiving data from JavaScript
port onStorageChange : (String -> msg) -> Sub msg

-- Using ports in update
type Msg
    = SaveData String
    | StorageChanged String

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        SaveData data ->
            ( model
            , saveToLocalStorage data
            )

        StorageChanged data ->
            ( { model | data = data }
            , Cmd.none
            )

-- Using ports in subscriptions
subscriptions : Model -> Sub Msg
subscriptions model =
    onStorageChange StorageChanged

JavaScript Side:

var app = Elm.Main.init({
    node: document.getElementById('app')
});

// Receiving from Elm
app.ports.saveToLocalStorage.subscribe(function(data) {
    localStorage.setItem('app-data', data);
});

// Sending to Elm
window.addEventListener('storage', function(e) {
    if (e.key === 'app-data') {
        app.ports.onStorageChange.send(e.newValue);
    }
});

Common Port Patterns:

-- Local storage
port setStorage : String -> Cmd msg
port getStorage : (String -> msg) -> Sub msg

-- WebSocket
port sendSocketMessage : String -> Cmd msg
port receiveSocketMessage : (String -> msg) -> Sub msg

-- Analytics
port trackEvent : { category : String, action : String } -> Cmd msg

-- Third-party libraries
port initMap : { lat : Float, lng : Float } -> Cmd msg
port updateMarker : { lat : Float, lng : Float } -> Cmd msg

Custom Elements

Embed Elm in existing applications using web components:

JavaScript Side:

customElements.define('elm-widget', class extends HTMLElement {
    connectedCallback() {
        this.app = Elm.Widget.init({
            node: this,
            flags: {
                initialValue: this.getAttribute('value')
            }
        });

        // Listen to Elm events
        this.app.ports.valueChanged.subscribe(value => {
            this.dispatchEvent(new CustomEvent('change', { detail: value }));
        });
    }
});

HTML Usage:

<elm-widget value="initial"></elm-widget>

Tooling and Build System

elm.json Configuration

Package vs Application:

{
    "type": "application",
    "source-directories": [
        "src"
    ],
    "elm-version": "0.19.1",
    "dependencies": {
        "direct": {
            "elm/browser": "1.0.2",
            "elm/core": "1.0.5",
            "elm/html": "1.0.0",
            "elm/http": "2.0.0",
            "elm/json": "1.1.3"
        },
        "indirect": {}
    },
    "test-dependencies": {
        "direct": {
            "elm-explorations/test": "1.2.2"
        },
        "indirect": {}
    }
}

Common Elm Commands

Building:

# Compile to JavaScript
elm make src/Main.elm --output=main.js

# Compile with optimization
elm make src/Main.elm --output=main.js --optimize

# Compile multiple files
elm make src/Main.elm src/Admin.elm --output=dist/

# Debug build
elm make src/Main.elm --output=main.js --debug

Development:

# Start development server
elm reactor

# Install packages
elm install elm/http
elm install elm/json
elm install elm/random

# Format code
elm-format src/ --yes

# Run tests
elm-test

Optimization:

# Build optimized
elm make src/Main.elm --output=main.js --optimize

# Minify with uglify-js
uglifyjs main.js --compress 'pure_funcs=[F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9],pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle --output main.min.js

Project Structure

Recommended Structure:

my-elm-app/
├── elm.json                 # Package configuration
├── src/
│   ├── Main.elm            # Entry point
│   ├── Models/
│   │   ├── User.elm
│   │   └── Post.elm
│   ├── Views/
│   │   ├── Home.elm
│   │   ├── Profile.elm
│   │   └── Common/
│   │       ├── Header.elm
│   │       └── Footer.elm
│   ├── Updates/
│   │   ├── User.elm
│   │   └── Post.elm
│   ├── Api/
│   │   ├── User.elm
│   │   └── Post.elm
│   ├── Utils/
│   │   ├── Validators.elm
│   │   └── Formatters.elm
│   └── Ports.elm
├── tests/
│   └── Tests.elm
├── public/
│   ├── index.html
│   ├── styles.css
│   └── assets/
└── README.md

Common Patterns

Form Handling

Complete Form Pattern:

type alias Form =
    { email : String
    , password : String
    , errors : List String
    , isSubmitting : Bool
    }

type FormMsg
    = UpdateEmail String
    | UpdatePassword String
    | SubmitForm
    | FormSubmitted (Result Http.Error User)

initForm : Form
initForm =
    { email = ""
    , password = ""
    , errors = []
    , isSubmitting = False
    }

updateForm : FormMsg -> Form -> (Form, Cmd FormMsg)
updateForm msg form =
    case msg of
        UpdateEmail email ->
            ( { form | email = email }
            , Cmd.none
            )

        UpdatePassword password ->
            ( { form | password = password }
            , Cmd.none
            )

        SubmitForm ->
            case validateForm form of
                Ok validForm ->
                    ( { form | isSubmitting = True, errors = [] }
                    , submitForm validForm
                    )

                Err errors ->
                    ( { form | errors = errors }
                    , Cmd.none
                    )

        FormSubmitted result ->
            case result of
                Ok user ->
                    ( initForm
                    , Cmd.none
                    )

                Err _ ->
                    ( { form
                        | isSubmitting = False
                        , errors = ["Submission failed"]
                      }
                    , Cmd.none
                    )

validateForm : Form -> Result (List String) Form
validateForm form =
    let
        errors =
            [ validateEmail form.email
            , validatePassword form.password
            ]
            |> List.filterMap identity
    in
    if List.isEmpty errors then
        Ok form
    else
        Err errors

viewForm : Form -> Html FormMsg
viewForm form =
    Html.form [ onSubmit SubmitForm ]
        [ input
            [ type_ "email"
            , value form.email
            , onInput UpdateEmail
            , disabled form.isSubmitting
            ]
            []
        , input
            [ type_ "password"
            , value form.password
            , onInput UpdatePassword
            , disabled form.isSubmitting
            ]
            []
        , div [ class "errors" ]
            (List.map viewError form.errors)
        , button
            [ type_ "submit"
            , disabled form.isSubmitting
            ]
            [ text (if form.isSubmitting then "Submitting..." else "Submit") ]
        ]

Routing (SPA Navigation)

URL Routing with Browser.application:

import Browser
import Browser.Navigation as Nav
import Url
import Url.Parser as Parser exposing (Parser, oneOf, s, int, (</>))

type Route
    = Home
    | Profile Int
    | Settings
    | NotFound

routeParser : Parser (Route -> a) a
routeParser =
    oneOf
        [ Parser.map Home Parser.top
        , Parser.map Profile (s "profile" </> int)
        , Parser.map Settings (s "settings")
        ]

fromUrl : Url.Url -> Route
fromUrl url =
    Parser.parse routeParser url
        |> Maybe.withDefault NotFound

type alias Model =
    { key : Nav.Key
    , route : Route
    }

type Msg
    = LinkClicked Browser.UrlRequest
    | UrlChanged Url.Url

init : () -> Url.Url -> Nav.Key -> (Model, Cmd Msg)
init _ url key =
    ( { key = key
      , route = fromUrl url
      }
    , Cmd.none
    )

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        LinkClicked urlRequest ->
            case urlRequest of
                Browser.Internal url ->
                    ( model
                    , Nav.pushUrl model.key (Url.toString url)
                    )

                Browser.External href ->
                    ( model
                    , Nav.load href
                    )

        UrlChanged url ->
            ( { model | route = fromUrl url }
            , Cmd.none
            )

view : Model -> Browser.Document Msg
view model =
    { title = "My App"
    , body =
        [ viewNavigation
        , viewRoute model.route
        ]
    }

viewRoute : Route -> Html Msg
viewRoute route =
    case route of
        Home ->
            viewHome

        Profile userId ->
            viewProfile userId

        Settings ->
            viewSettings

        NotFound ->
            div [] [ text "404 Not Found" ]

main : Program () Model Msg
main =
    Browser.application
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        , onUrlChange = UrlChanged
        , onUrlRequest = LinkClicked
        }

Debouncing User Input

Search with Debounce:

type alias Model =
    { searchQuery : String
    , searchDebounce : Int
    , results : List Result
    }

type Msg
    = UpdateSearch String
    | DebounceTick Time.Posix
    | PerformSearch
    | GotResults (Result Http.Error (List Result))

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        UpdateSearch query ->
            ( { model
                | searchQuery = query
                , searchDebounce = 3  -- Wait 3 ticks
              }
            , Cmd.none
            )

        DebounceTick _ ->
            if model.searchDebounce > 0 then
                ( { model | searchDebounce = model.searchDebounce - 1 }
                , Cmd.none
                )
            else if model.searchQuery /= "" then
                update PerformSearch model
            else
                ( model, Cmd.none )

        PerformSearch ->
            ( model
            , searchApi model.searchQuery
            )

        GotResults result ->
            case result of
                Ok results ->
                    ( { model | results = results }
                    , Cmd.none
                    )

                Err _ ->
                    ( model, Cmd.none )

subscriptions : Model -> Sub Msg
subscriptions model =
    Time.every 300 DebounceTick  -- Check every 300ms

Loading States

Remote Data Pattern:

type RemoteData error value
    = NotAsked
    | Loading
    | Success value
    | Failure error

type alias Model =
    { userData : RemoteData Http.Error User
    }

type Msg
    = FetchUser
    | GotUser (Result Http.Error User)

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        FetchUser ->
            ( { model | userData = Loading }
            , fetchUser
            )

        GotUser result ->
            case result of
                Ok user ->
                    ( { model | userData = Success user }
                    , Cmd.none
                    )

                Err error ->
                    ( { model | userData = Failure error }
                    , Cmd.none
                    )

viewUser : RemoteData Http.Error User -> Html Msg
viewUser userData =
    case userData of
        NotAsked ->
            button [ onClick FetchUser ] [ text "Load User" ]

        Loading ->
            div [ class "spinner" ] [ text "Loading..." ]

        Success user ->
            div [ class "user" ]
                [ h2 [] [ text user.name ]
                , p [] [ text user.email ]
                ]

        Failure error ->
            div [ class "error" ]
                [ text "Failed to load user"
                , button [ onClick FetchUser ] [ text "Retry" ]
                ]

Best Practices

Code Organization

  1. Keep Modules Small: Each module should have a single responsibility
  2. Use Type Aliases: Document complex types with meaningful names
  3. Expose Minimally: Only expose what's needed from modules
  4. Group Related Code: Keep related types, functions, and views together
  5. Consistent Naming: Use clear, consistent naming conventions

Type Safety

  1. Always Add Type Annotations: For top-level functions
  2. Use Custom Types: Instead of strings/ints for states
  3. Make Impossible States Impossible: Design types to prevent invalid states
  4. Leverage Maybe and Result: Never use defaults that hide errors
  5. Pattern Match Exhaustively: Cover all cases explicitly

Performance

  1. Use Html.Lazy: For expensive view functions that rarely change
  2. Optimize List Operations: Consider using Array for random access
  3. Batch Commands: Use Cmd.batch for multiple effects
  4. Minimize Subscriptions: Only subscribe when needed
  5. Profile Before Optimizing: Use elm reactor debugger

Testing

import Test exposing (..)
import Expect

suite : Test
suite =
    describe "User Validation"
        [ test "validates correct email" <|
            \_ ->
                validateEmail "test@example.com"
                    |> Expect.equal (Ok "test@example.com")

        , test "rejects invalid email" <|
            \_ ->
                validateEmail "invalid"
                    |> Expect.err

        , describe "Password validation"
            [ test "requires minimum length" <|
                \_ ->
                    validatePassword "short"
                        |> Expect.err

            , test "accepts valid password" <|
                \_ ->
                    validatePassword "securePassword123"
                        |> Expect.ok
            ]
        ]

Error Handling

  1. Use Result for Validation: Return meaningful error messages
  2. Handle All Http.Error Cases: Provide user-friendly messages
  3. Display Errors Clearly: Show validation errors next to form fields
  4. Provide Recovery Options: Offer retry buttons, alternative actions
  5. Log Errors via Ports: Send to external logging services

Accessibility

-- Use semantic HTML
viewButton : String -> msg -> Html msg
viewButton label msg =
    button
        [ onClick msg
        , ariaLabel label
        ]
        [ text label ]

-- Provide keyboard navigation
viewModal : Html msg
viewModal =
    div
        [ role "dialog"
        , ariaModal True
        , tabindex -1
        ]
        [ -- modal content
        ]

-- Use ARIA attributes
viewStatus : String -> Html msg
viewStatus message =
    div
        [ ariaLive "polite"
        , ariaAtomic True
        ]
        [ text message ]

Resources and Learning

Official Resources:

Key Packages:

  • elm/browser - Browser-based programs
  • elm/html - HTML generation
  • elm/http - HTTP requests
  • elm/json - JSON encoding/decoding
  • elm/random - Random value generation
  • elm/time - Time and dates
  • elm/url - URL parsing and building
  • elm-explorations/test - Testing framework

Learning Resources:

  • Elm in Action (book)
  • Elm Radio (podcast)
  • Elm Weekly (newsletter)
  • Exercism Elm Track

Skill Version: 1.0.0 Last Updated: November 2025 Skill Category: Functional Programming, Web Development, Type-Safe Programming Compatible With: JavaScript, Web Components, SPA Frameworks