I’ve been asked by a number of folks: “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.
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, & 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 & the actual requests)
Tinder is essentially 3 things (which are coincidently the 3 views):
- A user profile
- A platform for viewing & passing judgment on other profiles
- Messaging between users that have matched
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:
- Some of my features are very small & 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)
- What if I want to change frameworks & their model for the application no longer fits this paradigm?
- 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 & 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
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 & it’s easier (and requires a ton less
import`s) 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.
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
: This is where I bundle up all of the REST calls to abstract away the URLs, their building, & their types.
: Self-contained widget-y things like custom implementation of a drop-down menu, or a video player. They should be reusable & consumable at any level.
: If you need translations, a folder for internationalization phrases wouldn’t be a bad idea.
: Usually there are types & 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 & a
Message.elm which would describe the structure of a message.
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.