Skip to content
elm-development logo

Elm Development

elm-development

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

SKILL.md

Full skill instructions

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 "[email protected]"
                    |> Expect.equal (Ok "[email protected]")

        , 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

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View