23 Jan 2019
6 min read
NOTE: This post was originally written in 2017. I have since updated the code for this project to Elm 0.19. See my talk at Oslo Elm Day 2019 for an explanation of what changed!
People often ask me if I can point them to an open-source Elm Single Page Application so they can peruse its code.
Ilias van Peer linked me to the Realworld project, which seemed perfect for this. They provide a back-end API, static markup, styles, and a spec, and you build a SPA front-end for it using your technology of choice.
Here's the result. I had a ton of fun building it!
Fair warning: This is not a gentle introduction to Elm. I built this to be something I'd like to maintain, and did not hold back. This is how I'd build this application with the full power of Elm at my fingertips.
🎥 I gave a talk at Elm Europe about the principles I used to build this, and I highly recommend watching it! It's called Scaling Elm Apps[https://www.youtube.com/watch?v=DoA4Txr4GUs].
I went with a routing design that optimizes for user experience. I considered three use cases, illustrated in this gif:
The use cases:
On fast connections, I want users to transition from one page to another seamlessly, without seeing a flash of a partially-loaded page in between.
To accomplish this, I had each page expose
init : Task PageLoadError Model. When the router receives a request to transition to a new
page, it doesn't transition immediately; instead, it first calls
Task.attempt on this
init task to fetch the data the new page
If the task fails, the resulting
PageLoadError tells the router what
error message to show the user. If the task succeeds, the resulting
Model serves as the initial model necessary to render 100% of the
new page right away.
No flash of partially-loaded page necessary!
On slow connections, I want users to see a loading spinner, to reassure them that there's something happening even though it's taking a bit.
To do this, I'm rendering a loading spinner in the header as soon as
the user attempts to transition to a new page. It stays there while
Task is in-flight, and then as soon as it resolves (either to
the new page or to an error page), the spinner goes away.
For a bit of polish, I prevented the spinner from flashing into view
on fast connections by adding a CSS
to the spinner's animation. This meant I could add it to the DOM as
soon as the user clicked the link to transition (and remove it again
once the destination page rendered), but the spinner would not become
visible to the user unless a few hundred milliseconds of delay had
elapsed in between.
I'd like at least some things to work while the user is offline.
I didn't go as far as to use Service Worker (or for that matter App Cache, for those of us who went down that bumpy road [https://www.youtube.com/watch?v=WqV5kqaFRDU]), but I did want users to be able to visit pages like New Post which could be loaded without fetching data from the network.
init returned a
Model instead of a
Task PageLoadError Model. That was all it took.
We have over 100,000 lines of Elm code in production at NoRedInk, and we've learned a lot along the way! (We don't have a SPA, so our routing logic lives on the server, but the rest is the same.) Naturally every application is different, but I've been really happy with how well our code base has scaled, so I drew on our organizational scheme when building this app.
Keep in mind that although using
exposing to create guarantees by
restricting what modules expose [https://youtu.be/IcgmSRJHu_8] is an
important technique (which I used often here), the actual file
structure is a lot less important. Remember, if you change your mind
and want to rename some files or shift directories around, Elm's
compiler will have your back. It'll be okay!
Here's how I organized this application's modules.
These modules hold the logic for the individual pages in the app.
Pages that require data from the server expose an
which returns a
Task responsible for loading that data. This lets
the routing system wait for a page's data to finish loading before
switching to it.
These modules hold reusable views which multiple
Page modules import.
Views.User, are very simple. Others, like
Views.Article.Feed, are very complex. Each exposes an appropriate
API for its particular requirements.
Views.Page module exposes a
frame function which wraps each
page in a header and footer.
These modules describe common data structures, and expose ways to
translate them into other data structures.
Data.User describes a
User, as well as the encoders and decoders that serialize and
User to and from JSON.
Identifiers such as
Slug - which are
used to uniquely identify comments, users, and articles, respectively
type alias Username = String, we could mistakenly pass a
Usernameto an API call expecting a
Slug, and it would still compile. We can rule bugs like that out by implementing identifiers as union types.
These modules expose functions to make HTTP requests to the app
server. They expose
Http.Request values so that callers can combine
them together, for example on pages which need to hit multiple
endpoints to load all their data.
I don't use raw API endpoint URL strings anywhere outside these
Request.* modules should know about actual endpoint
This exposes functions to translate URLs in the browser's Location bar to logical "pages" in the application, as well as functions to effect Location bar changes.
Similarly to how
Request modules never expose raw API URL strings,
this module never exposes raw Location bar URL strings either. Instead
it exposes a union type called
Route which callers use to specify
which page they want.
Centralizing all the ports in one
port module makes it easier to
keep track of them. Most large applications end up with more than just
two ports, but in this application I only wanted two. See
At NoRedInk our policy for both ports and flags is to use
This way we have full control over how to deal with any surprises in
the data. I followed that policy here.
This kicks everything off, and calls
Html.map on the
Page modules to switch between them.
Based on discussions around how asset management features like code splitting and lazy loading have been shaping up, I expect most of this file to become unnecessary in a future release of Elm.
These are miscellaneous helpers that are used in several other modules.
It might be more honest to call this
elm-cssto style it. However, since Realworld provided so much markup, I ended up using
html-to-elmto save myself a bunch of time instead.
elm-testin progress, and I'd like to use the latest and greatest for tests. I debated waiting until the new
elm-testlanded to publish this, but decided that even in its untested form it would be a useful resource.
I hope this has been useful to you!
And now, back to writing another chapter of Elm in Action 😉
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!