I’ve been asked by a number of people: β€œWhat does the proper Elm file structure look like?” While the answer may be a bit nuanced as the answer is often dependent on your own project, I can show you what’s working for me after trying a bunch of different styles.

Mandlebrot set

What I’ve found in trying different patterns is that a structure that is built around features, has some room for globals, but is mostly fractal in design allows for scalability, refactorability, and reason-about-ability.


So let’s talk about a sample problem I’ve toyed around withβ€”hitting the Tinder API

Let’s break down the problem

(Not thinking about authentication and the actual requests)

Tinder is essentially 3 things (which are coincidently the 3 views):


A bad idea

Tinder
β”œβ”€β”€ Apis …
β”œβ”€β”€ Components …
β”œβ”€β”€ I18n …
β”œβ”€β”€ Models
β”‚   β”œβ”€β”€ Model.elm
β”‚   β”œβ”€β”€ Profile.elm
β”‚   β”œβ”€β”€ Judgment.elm
β”‚   └── Messaging.elm
β”œβ”€β”€ Msgs
β”‚   β”œβ”€β”€ Msg.elm
β”‚   β”œβ”€β”€ Profile.elm
β”‚   β”œβ”€β”€ Judgment.elm
β”‚   └── Messaging.elm
β”œβ”€β”€ Routes
β”‚   β”œβ”€β”€ Route.elm
β”‚   β”œβ”€β”€ Profile.elm
β”‚   β”œβ”€β”€ Judgment.elm
β”‚   └── Messaging.elm
β”œβ”€β”€ Routing
β”‚   β”œβ”€β”€ Routing.elm
β”‚   β”œβ”€β”€ Profile.elm
β”‚   β”œβ”€β”€ Judgment.elm
β”‚   └── Messaging.elm
β”œβ”€β”€ Types …
β”œβ”€β”€ Updates
β”‚   β”œβ”€β”€ Update.elm
β”‚   β”œβ”€β”€ Profile.elm
β”‚   β”œβ”€β”€ Judgment.elm
β”‚   └── Messaging.elm
└── Views
    β”œβ”€β”€ Views.elm
    β”œβ”€β”€ Profile.elm
    β”œβ”€β”€ Judgment.elm
    └── Messaging.elm

This is the kind of structure that I knee-jerked towards when I first got jumped into project that finally took up more than a single file. You’ll see this kind of pattern often floating around other front-end frameworks you may know. But not building around features presents us with 3 problems:

  1. Some of my features are very small and don’t require a separate file for each of the frameworks ideas? (I might keep all of it in one file but separate the view into something else)

  2. What if I want to change frameworks and their model for the application no longer fits this paradigm?

  3. What if I have sub-features? (Nested routes, views, etc.)

Using a Sample Feature-Based Structure

Tinder
β”œβ”€β”€ Apis …
β”œβ”€β”€ Components …
β”œβ”€β”€ I18n …
β”œβ”€β”€ Judgment
β”‚   β”œβ”€β”€ Model.elm
β”‚   β”œβ”€β”€ Msg.elm
β”‚   β”œβ”€β”€ Route.elm
β”‚   β”œβ”€β”€ Routing.elm
β”‚   β”œβ”€β”€ Update.elm
β”‚   └── View.elm
β”œβ”€β”€ Messaging
β”‚   β”œβ”€β”€ Overview
β”‚   β”‚   β”œβ”€β”€ Overview.elm
β”‚   β”‚   └── View.elm
β”‚   β”œβ”€β”€ Message
β”‚   β”‚   β”œβ”€β”€ Message.elm
β”‚   β”‚   └── View.elm
β”‚   └── Messaging.elm
β”œβ”€β”€ Profile
β”‚   β”œβ”€β”€ Model.elm
β”‚   β”œβ”€β”€ Msg.elm
β”‚   β”œβ”€β”€ Route.elm
β”‚   β”œβ”€β”€ Routing.elm
β”‚   β”œβ”€β”€ Update.elm
β”‚   └── View.elm
β”œβ”€β”€ Types …
β”œβ”€β”€ Model.elm
β”œβ”€β”€ Msg.elm
β”œβ”€β”€ Route.elm
β”œβ”€β”€ Routing.elm
β”œβ”€β”€ Update.elm
└── View.elm

I’m not saying I’d do it exactly this way, but it’s a solution. Here you can see how the root level and the feature levels mimic each other’s structure. What you then do at the root level’s piece is merely forward along just what each feature needs letting it have a self-contain-y feel to it. To explain an example, Update would contain an update function where given a Profile.Msg(..) type, we can then call Profile.Update.update with the appropriate Msg and Model.

In the Messaging folder you can see that there’s a subfolder structure in which in this example would hold all the pieces but the View.elm because maybe it’s not that complicated and it’s easier (and requires a ton less imports) to build all that functionality. At Messaging.Messaging we could have some basic forwarding logic to each sub piece just like root-level stuff, continuing this recursive design.

Global Folders

Certain parts of your application will have data thats shared between all of the featuresβ€”this is why we have some global folders. For the few projects I’ve touched these folders include

Apis

This is where I bundle up all of the REST calls to abstract away the URLs, their building, and their types.

Components

Self-contained widget-y things like custom implementation of a drop-down menu, or a video player. They should be reusable and consumable at any level.

I18n

If you need translations, a folder for internationalization phrases wouldn’t be a bad idea.

Types

Usually there are types and type aliases you be dealing with all over the application. In the Tinder example a file for User.elm would be useful for describing a user and a Message.elm which would describe the structure of a message.


Takeaway

So, just like the Mandelbrot set is a recursive structure that contains more copies of itself, we too can build our project to look about the same on every level which not only scales better as our project becomes larger, but also is easier to think about the app since each child part mimics its parent.