Yet Another Tour of an Open-Source Elm SPA
Tags: elm, spa, webapp, scaling
Published: 2024-04-22 14:06:58

---
title: Yet Another Tour of an Open-Source Elm SPA
published: true
description: A tour of dwayne/elm-conduit, an open-source Elm SPA.
tags: elm, spa, webapp, scaling
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/633u5rmeazmffgqiptir.png
# Use a ratio of 100:42 for best results.
# published_at: 2024-04-22 10:00 +0000
---

## Introduction

About 7 years ago, in the midst of writing [Elm in Action](https://www.manning.com/books/elm-in-action), Richard Feldman developed [rtfeldman/elm-spa-example](https://github.com/rtfeldman/elm-spa-example), wrote [Tour of an Open-Source Elm SPA](https://dev.to/rtfeldman/tour-of-an-open-source-elm-spa) and graciously shared both of them with the Elm community. The community's response was overwhelmingly positive and it was clear that he had addressed a major need. If you were one of the many web application developers asking "Where can I find an open-source example of an Elm Single Page Application?", then, the Elm SPA Example instantly became the canonical example that everyone was going to point you towards. This was a landmark achievement in the history of Elm.

However, since 2018, Richard set clear [expectations](https://github.com/rtfeldman/elm-spa-example/issues/21#issuecomment-373542013) on the effort he would be able to devote to the repository. And, nearing the end of 2023 it became clear that [his focus shifted towards Roc and raising a family](https://youtu.be/DzhIprQan68?feature=shared&t=157).

> I'd really like to go back and update elm-spa-example. ... After a while, it's like, yeah this is never happening.
>
> &mdash; Richard Feldman at [3:23](https://youtu.be/DzhIprQan68?feature=shared&t=203)

Fast-forward to now, circa April 2024, and unsurprisingly, the repository has been unmaintained for 5 years and the demo is broken in a few places.

In light of all this, it became exceedingly clear that someone else needed to step in and help. Why not me? Well, it can be me. And, after [3 months of development](https://github.com/dwayne/elm-conduit/commit/fe4552ff6ec2cd85de0f25a706dd03a481af1c8d), I am happy to announce ([again](https://discourse.elm-lang.org/t/announcing-dwayne-elm-conduit-a-replacement-for-rtfeldman-elm-spa-example/9758)) [dwayne/elm-conduit](https://github.com/dwayne/elm-conduit) ([demo](https://elm-conduit.netlify.app/)), an open-source Elm SPA for [RealWorld](https://github.com/gothinkster/realworld)'s Medium.com clone.

## [dwayne/elm-conduit](https://github.com/dwayne/elm-conduit) ([demo](https://elm-conduit.netlify.app/))

[dwayne/elm-conduit](https://github.com/dwayne/elm-conduit) is built from scratch using the full power of [Elm](https://elm-lang.org/), no holds barred. This is how I would architect and build a reliable, maintainable, and scalable production-ready Elm web application.

It uses [devbox](https://www.jetify.com/devbox/), Elm 0.19.1, the latest Elm packages (in particular [elm/http 2.0.0](https://package.elm-lang.org/packages/elm/http/2.0.0/)), [elm-review](https://github.com/jfmengels/elm-review), [Caddy](https://caddyserver.com/), a sprinkle of [Dart Sass](https://sass-lang.com/dart-sass/), and [a handful of Bash scripts](https://github.com/dwayne/elm-conduit/tree/733615064128bc767cf68088a82f6076c3dd2e83/bin) (one of them being [a deployment script](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/bin/deploy-production)). It uses [elm test](https://package.elm-lang.org/packages/elm-explorations/test/latest) and features [tests for key data structures](https://github.com/dwayne/elm-conduit/tree/733615064128bc767cf68088a82f6076c3dd2e83/tests/Test/Data).

In short, if you were asking:

> Where can I find an open-source example of an Elm Single Page Application that is well-maintained, uses the latest Elm libraries and tooling, and has a build and deployment story?

Then, [dwayne/elm-conduit](https://github.com/dwayne/elm-conduit) is your answer.

{% embed https://youtu.be/0LAML9FUv84 %}

## Process

I started out with an HTML/CSS prototype, built the views in a [Storybook](https://storybook.js.org/)-like sandbox and finally put it all together with domain logic, interactivity, and API requests.

### HTML/CSS prototype

This step was made easier by the fact that [RealWorld](https://github.com/gothinkster/realworld) already had [frontend specs with templates](https://realworld-docs.netlify.app/docs/specs/frontend-specs/templates). So I didn't have to develop the HTML/CSS from scratch. However, I still made a few tweaks to the existing templates.

{% embed https://youtu.be/PjeFqNWgle8 %}

*The [source code](https://github.com/dwayne/elm-conduit/tree/733615064128bc767cf68088a82f6076c3dd2e83/prototype) for the prototype.*

### Storybook-like sandbox

This is the step where I went from HTML/CSS to Elm. The goal of this step was to realize all the views in Elm. I've found that if I'm able to independently work with the views outside the context of the application then I can be 100% confident that I'm not introducing unnecessary dependencies between my view code and the domain logic.

During this step I also got hints of the supporting data structures, the `Data.*` modules, that would be needed.

{% embed https://youtu.be/8AWH-MpnetQ %}

*The [source code](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Sandbox.elm) for the sandbox.*

### Domain logic, interactivity, and API requests

Finally, it's in this step where I started to build out [`Main`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Main.elm). I built the router, [Data.Route](), the reusable [`Api`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Api.elm) module, and the port infrastructure, [Lib.Port.Message](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Lib/Port/Message.elm) and [Port.Outgoing](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Port/Outgoing.elm). From there, I constructed the pages from least to most difficult based on what I learned when building the sandbox. As I put together a page, I would continue to flesh out `Main` (the page coordinator), [`Data.*`](https://github.com/dwayne/elm-conduit/tree/733615064128bc767cf68088a82f6076c3dd2e83/src/Data) (domain specific data structures), [`Lib.*`](https://github.com/dwayne/elm-conduit/tree/733615064128bc767cf68088a82f6076c3dd2e83/src/Lib) (reusable library functions and data structures), and [`Api.*`](https://github.com/dwayne/elm-conduit/tree/733615064128bc767cf68088a82f6076c3dd2e83/src/Api) (the [Conduit API](https://realworld-docs.netlify.app/docs/specs/frontend-specs/swagger)) as needed for that page to be completed.

## Module structure

![The module structure for dwayne/elm-conduit.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fnolno885q02p9m2wr8g.png)

When Richard first wrote [rtfeldman/elm-spa-example](https://github.com/rtfeldman/elm-spa-example) he used a module structure similar to what you see above. However, when [he upgraded his application to Elm 0.19](https://github.com/rtfeldman/elm-spa-example/commit/63062273d7a267aeac0e518b1c239ebf3e15a01b) he decided to completely rearchitect everything and move to [a different structure](https://github.com/rtfeldman/elm-spa-example/tree/63062273d7a267aeac0e518b1c239ebf3e15a01b/src). Why? [We don't know](https://github.com/rtfeldman/elm-spa-example/issues/53). But, whatever his reasons, I'd still be partial to the original structure because you end up with a cleaner separation of concerns and a "[louder architecture](https://blog.cleancoder.com/uncle-bob/2011/09/30/Screaming-Architecture.html)".

### The `Main` module

This is the entry point to the application and the page coordinator.

When you first load the application in the browser, it is `Main` that determines whether or not you're logged in. It [decodes your configuration](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Main.elm#L103), passed through [flags](https://guide.elm-lang.org/interop/flags), and tries to determine who's logging in.

It uses your URL from the browser's location bar to determine [which page](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Main.elm#L213-L220) you're trying to access.

### The `Page.*` modules

*Examples:* `Page.Article`, `Page.Editor`, `Page.Home`, `Page.Login`, `Page.Profile`

These modules contain the logic for the individual pages in the web application. A page is [responsible for fetching its own data](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Page/Home.elm#L126-L130) and for composing [its look](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Page/Home.elm#L422-L443).

### The `View.*` modules

*Examples:* `View.ArticlePreview`, `View.AuthErrors`, `View.Comment`, `View.FollowButton`, `View.Tabs`, `View.TagInput`

Each module contains one or more reusable view functions, fully decoupled from the domain logic, which multiple `Page` modules import.

Some, like [`View.AuthErrors`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/View/AuthErrors.elm), are very simple. Others, like [`View.Navigation`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/View/Navigation.elm), are a little more complex. Each exposes an appropriate API for its particular requirements. All are featured in the sandbox.

Interestingly, no `View.*` module needed an update function. This correlates exactly with [this user's perceptive commentary](https://github.com/gothinkster/realworld/issues/262#issue-357775710).

### The `Api.*` modules

*Examples:* `Api.CreateArticle`, `Api.GetArticles`, `Api.Login`, `Api.UpdateUser`

These modules expose functions to make HTTP requests to the application server.

There is a nice correspondence between the `Api.*` modules and the API as described by the [Conduit API documentation](https://realworld-docs.netlify.app/docs/specs/frontend-specs/swagger/). Except for [`Api.GetArticles`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Api/GetArticles.elm), each module contains the data structures and logic necessary to deal with a single API endpoint.

`Api.GetArticles` wraps `GET /articles/feed` and `GET /articles` into a single function, [`getArticles`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Api/GetArticles.elm#L71-L78), that makes it easy to get an article in all the ways possible without making a mistake. See its [`Request`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Api/GetArticles.elm#L34-L36) opaque type for further details.

The `Api.*` modules build upon the [`Api`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Api.elm) module. The key function in the `Api` module is the private function, [`expectJson`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Api.elm#L206-L257), that describes how to deal with every possible response, good or bad, we could get from the Conduit API.

### The `Data.*` modules

*Examples:* `Data.Article`, `Data.Comment`, `Data.Pager`, `Data.Route`, `Data.Timestamp`, `Data.Validation`

These modules describe common data structures used throughout the application. Some of the modules expose type aliases with a few helper functions, for e.g. [`Data.Article`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Data/Article.elm). Other modules expose opaque types with smart constructors and useful functions that work on the data type, for e.g. [`Data.Pager`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Data/Pager.elm).

[`Data.Route`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Data/Route.elm) exposes a function, [`fromUrl`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Data/Route.elm#L31-L58), to translate URLs from the browser's location bar into a valid route, as well as a function, [`toString`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Data/Route.elm#L81-L109), that converts a valid route back into a valid path used by the application. `fromUrl` is extensively [tested](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/tests/Test/Data/Route.elm#L18). But, `toString` is kind of hard to get wrong due to the type system, so I decided not to test it.

### Ports

Three things need to be done over in JavaScript land.

1. When a user logs in we need to save their token in `localStorage`.
2. When a user logs out we need to remove their token from `localStorage`.
3. When any unexpected errors occur we need to send the details over to an error logging service, for e.g. like [Rollbar](https://rollbar.com).

This suggests we need three ports. However, through multiplexing, we can reduce it to one port. How?

In the `port module`, [`Port.Outgoing`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Port/Outgoing.elm), I create one outgoing port called [`send`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Port/Outgoing.elm#L30) that takes, as input, an arbitrary JSON value. Then, I use a general JSON message format, defined in [`Lib.Port.Message`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Lib/Port/Message.elm), to communicate with the JavaScript side. `Port.Outgoing` exposes three commands, `deleteToken`, `logError`, and `saveToken`, whose side-effect is to send a message over the `send` port.

For e.g. [`saveToken token`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Port/Outgoing.elm#L14-L17) might send the following message:

```json
{
	"tag": "saveToken"
	"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxMjc3Nn0sImlhdCI6MTcxMzc4MzQ4NywiZXhwIjoxNzE4OTY3NDg3fQ.Pqfubt_-KRujNhiXr-vwSvAhvlyu8IhV6eXw2iEdHzF"
}
```

The approach scales. We do [a similar thing at Qoda](https://discourse.elm-lang.org/t/a-demo-of-qoda-and-an-explanation-of-how-we-use-ports/8978). A naive use of ports would require 161 total ports. But, through multiplexing, we only ever use two ports, one outgoing and one incoming.

### The `Lib.*` modules

*Examples:* `Lib.Browser.Dom`, `Lib.Html.Attributes`, `Lib.Either`, `Lib.OrderedSet`, `Lib.Task`, `Lib.Validation`

These modules contain helper functions and data structures that are needed to implement this project but that are general enough to be extracted and reused in other projects.

[`Lib.Html.Attributes`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Lib/Html/Attributes.elm) exposes an [`attrList`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Lib/Html/Attributes.elm#L6-L16) function that allows you to add both required and optional HTML attributes to an HTML element. The optional HTML attributes can easily be added and removed depending on the boolean value it is paired with.

[`Lib.Task`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Lib/Task.elm) exposes the [`dispatch`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Lib/Task.elm#L6-L8) function I use for parent-child communication. For e.g. it is used by `Page.Login` to tell `Main` when you've [logged in](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/src/Page/Login.elm#L115).

## Tests

By using Elm and working with the type system I eliminated whole categories of tests but not all. The [`tests/Test`](https://github.com/dwayne/elm-conduit/tree/733615064128bc767cf68088a82f6076c3dd2e83/tests/Test) contains the unit and fuzz tests that, I found, were necessary for me to gain confidence in the correctness of the web application. I used [`elm-explorations/test`](https://package.elm-lang.org/packages/elm-explorations/test/2.2.0/) to write the unit and fuzz tests.

[`Test.Data.Comments`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/tests/Test/Data/Comments.elm), [`Test.Data.Pager`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/tests/Test/Data/Pager.elm), and [`Test.Data.Timestamp`](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/tests/Test/Data/Timestamp.elm) contain interesting tests. For e.g. `Test.Data.Comments` has [a fuzz test](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/tests/Test/Data/Comments.elm#L17-L31) that ensures when we decode the comments from the API we always get it sorted in reverse chronological order.

## Build and deployment

I reused my build and deployment scripts from [dwayne/elm-hello](https://github.com/dwayne/elm-hello). 

For this application, Elm controlled the routing. So, I had to adapt the scripts to deploy to [Netlify](https://www.netlify.com/) instead of [GitHub Pages](https://pages.github.com/). Why? Because you need to be able to tell the web server to [redirect all relevant requests to the application](https://github.com/dwayne/elm-conduit/blob/production/_redirects#L1). GitHub Pages doesn't have support for it.

[deploy-production](https://github.com/dwayne/elm-conduit/blob/733615064128bc767cf68088a82f6076c3dd2e83/bin/deploy-production) is the deployment script. It makes use of `git worktree` to commit a production build to a separate [production](https://github.com/dwayne/elm-conduit/tree/production) branch. Netlify is configured to deploy from this branch anytime it changes. Read Kris Jenkins' [Git for Static Sites](http://blog.jenkster.com/2016/02/git-for-static-sites.html) to learn more.

## Conclusion

[dwayne/elm-conduit](https://github.com/dwayne/elm-conduit) is a modern open-source example of an Elm Single Page Application that is written purely in Elm with no frameworks, is production-ready, has 100% coverage of the RealWorld spec, provides examples of useful tests used in a web application context, uses the latest Elm libraries and tooling, and has a build and deployment story.

I hope this would be useful to anyone who's learning Elm and trying to write a non-trivial SPA.

Enjoy!