A general and flexible project structure that works for all projects in any ecosystem.
NullVoxPopuli
Posted on September 28, 2018
To quote another article on a similar topic:
the ideal structure is the one that allows you to move around your code with the least amount of effort.
Why worry about folder/file structure at all? It seems like a difficult problem to solve. When there are no restrictions, almost everyone has a different idea of how 'things' should be named, and where they should live. In order to get everyone on the same page to achieve maximum project consistency, a structure should be agreed upon beforehand.
There are many topics on file structure. None of them agree. Some may have some similar concepts. Some may be too relaxed to be worthwhile. Ultimately, when faced with the choice of where to put a file, everyone's preference seems to be a little different.
So, how is this article going to be any different? My goal is to define a set of criteria for which we can assess a folder/file structure, and then to describe a reasonable start to a structure that can work as a base for any single-page-app in any ecosystem -- React, Vue, Angular, or Ember.
Firstly, let's define the criteria that we'll assess structures with.
- Users should be able to maintain their apps without worrying about the structure of their imports inhibiting them from making changes.
- Related files should be discoverable, such that a user does not need to hunt for a file should they not be using TypeScript (where you'd be able to use "Go to definition"
- Related files should be accessible, such that a user can easily locate a related file without having any IDE features (i.e.: browsing on github).
- Users should have reasonable context at any level within their project hierarchy. Flattening out too much is overwhelming and reduces the ability to maintain, discover, and access.
- Refactoring sections of the project should be easy. When moving a directory to a new location, the internal behavior should remain functional.
- The right way and place to add a new thing should be obvious and a structure should not allow for unnecessary decisions.
- Tests and styles should be co-located along side components.
- Avoid the infamous "titlebar problem", where a bunch of files all named the same can't be differentiated in the editor (though, a lot of this is editor-based)
- The structure should not impose limitations that would prevent technical advancement -- such as the addition of code-splitting to a project that does not yet have it.
The general-enough-to-work-for-all-apps-layout:
Note that any combination of {folder-name}/component.js,template.hbs
should be synonymous with:
- React:
{folder-name}/index.jsx,display.jsx
- Vue:
{folder-name}/index.vue,display.vue
- Angular:
{folder-name}/component.js,template.html
- Ember:
{folder-name}/component.js,template.hbs
- etc
Also, note in these examples are shorthand, and some projects (particularly Angular projects), like to be very explicit with naming, such as ComponentName/ComponentName.Component.js
.
src
├── data
├── redux-store
├── ui
│ ├── components
│ │ └── list-paginator
│ │ ├── paginator-control
│ │ │ ├── component.js
│ │ │ └── template.hbs
│ │ ├── component.js
│ │ ├── integration-test.js
│ │ └── template.hbs
│ ├── routes
│ │ ├── login
│ │ │ ├── acceptance-test.js
│ │ │ ├── route.js
│ │ │ └── template.hbs
│ │ └── post
│ │ ├── -components
│ │ │ └── post-viewer
│ │ │ ├── component.js
│ │ │ └── template.hbs
│ │ ├── edit
│ │ │ ├── -components
│ │ │ │ ├── post-editor
│ │ │ │ │ ├── calculate-post-title.js
│ │ │ │ │ ├── component.js
│ │ │ │ │ └── template.hbs
│ │ │ │ ├── route.js
│ │ │ │ └── template.hbs
│ │ │ ├── route.js
│ │ │ └── template.hbs
│ │ ├── route.js
│ │ └── template.hbs
│ ├── styles
│ │ └── app.scss
│ └── index.html
└── utils
└── md5.js
Going though the folders from top to bottom, because dev.to doesn't allow inline links without code fences... (a great feature of one of prism.js' plugins.
src
Most of this will focus on the src
directory, as any other top-level folder or file tends to be more project or ecosystem specific, and may not generally translate to projects cross-ecosystem. Some examples of those folders that may not translate due to project-specific or build-configuration-specific reasons are: app/
, tests/
, vendor/
, public/
, config/
, translations/
, etc.
src/data
This directory is intended for all api-related data interactions and representations. In an app where you have the model-adapter-serializer pattern, you may want additional folders within src/data
such as models
or transforms
, depending on how much normalization you desire within your application. This is why it doesn't necessarily make sense to have anything named more specific or vague.
src/redux-store
If using redux, most guides and tutorials just use the same store
, which can be ambiguous, since store
is a construct used by any library that maintains a cache of data. So not only in Redux, but also in Orbit.js, and ember-data.
For more info on app-level state management, See this article comparing state mangement in both React and Ember
src/ui
The entirety of anything that directly affects the display should go in the ui
folder. This includes styles, components, and routes. The user interface can exist independent of data, application state, and utilities.
src/ui/routes
Most single page apps are using some sort of router, and therefor the UI is entirely route based. What components display are determined by what routes are active. Due to this coupling of display, and consequently, behavior with the browser URL, it should only be natural to divide up your app by the natural route boundaries. Splitting the UI by route also lends itself to straight-forward code-splitting on the route boundaries.
src/ui/routes/{route-name}/-components
In a recent React project, I've tried to omit the route-level private components directory, but it's lead to confusion between what is intended for the route, and what is there to support what is rendered on the route. I had originally omitted the -components
directory thinking that if I/my team just use the right folders, things wouldn't be so bad.
An example of a page where you'd want nested routes separate from your components is tabbed navigation:
posts/post
├── view/
├── comment-moderation/
├── publishing-options/
│ ├── -components/
│ │ ├── confirm-publish-modal.jsx
│ │ └── social-media-blast-options.jsx
│ └── index.jsx
└── edit/
├── -components/
└── index.jsx
This structure, unlike the above link (things wouldn't be so bad), this has a clear, explicit separation of components and route-specific components. In the linked react app, I've also been playing with keeping local-only higher-order components (HoCs) at the top route-level due to their one-time use nature -- though, in this particular app, commonly-used HoCs are moved to the data directory. I'm still kind of playing around with the idea, but the HoC locations are more specific to the functional single-page-apps such as those that would be react-based.
One criteria to use to know if your structure is heading in the right direction is how often you end up using ../
or ../../
in your import paths. Using upwards reverse relative paths violates our Goal #5
stating that any subtree can change location and the functionality of the contents should remain in a working state. The above example should not inherently have any reverse-relative pathing.
An example violating Goal #5
:
posts/post
├── view/
├── comment-moderation/
├── publishing-options/
│ └── index.jsx
├── confirm-publish-modal.jsx
├── social-media-blast-options.jsx
└── edit/
└── index.jsx
Here, publishing-options
files must use ../
to access the components defined at the parent level.
src/utils
Any functions, classes, or utilities should live in src/utils
. These files should be purely unit testable, as they should have no app dependencies. This includes things like string format conversion, auth0 wrappers, fetch
abstractions, etc.
Overall
Let's revisit our goals, and look out how this proposed layout meets each one:
1) Users should be able to maintain their apps without worrying about the structure of their imports inhibiting them from making changes.
Achieving this goal is mostly through simply having any documented convention that can be referenced later. There are currently no general static analysis tools to help out with enforcing a structure -- though, there is one tool for one of the major frameworks that dictates structure. (See Implementation below)
2) Related files should be discoverable, such that a user does not need to hunt for a file should they not be using TypeScript (where you'd be able to use "Go to definition"
By having related files next to each other in this layout, everything is contextual by nature. If someone is a heavy file-tree/project-tree browser, they'll have an easy time navigating and discovering what they're working on and what is involved.
3) Related files should be accessible, such that a user can easily locate a related file without having any IDE features (i.e.: browsing on github).
This is related to (2), but more enforces co-location. When browsing files quickly online, without editor or typescript features, it's convenient to be able to click through as few web pages as possible to view related components.
4) Users should see have reasonable context at any level within their project hierarchy. Flattening out too much _is overwhelming and reduces the ability to maintain, discover, and access._
By having a nested structure by route, any component that is only used in one place will by contextually co-located to its usage. This keeps the amount of large flat folders to a minimum, and allows for understand the greater design of the app without having to follow references everywhere. Sibling folders are to be treated as complete unrelated (adopted?).
5) Refactoring sections of the project should be easy. When moving a directory to a new location, the internal behavior should remain functional.
I hope this one is self-explanatory, but this folder/file structure allows for drag-and-drop refactoring where any folder moved should have all of its internal tests still passing.
6) The right way and place to add a new thing should be obvious and a structure should not allow for unnecessary decisions.
This, in part, relies on both documentation and programmatic enforcement. The structure follows a strict set of rules that can be easily learned. For example, when using this folder/file stricture, by default, things should be going in -components
folders as you build out a route. For more inspiration on what kind of rules there could be, read about The Octane layout (formally Module Unification)
7) Tests and styles should be co-located along side components.
Instead of in a top-level tests/
directory, tests can be contextually located with the thing that they are testing. This works for unit, integration, and acceptance tests. There will, of course, be exceptions to this, where you may be testing something app-wide and it has no specific context -- for those situations, I tend to just put tests in tests/acceptance/
(if they are acceptance tests).
8) Avoid the infamous "titlebar problem", where a bunch of files all named the same can't be differentiated in the editor (though, a lot of this is editor-based)
The tab problem shouldn't be a thing in modern editors
(neo)Vim:
VSCode:
Atom:
9) The structure should not impose limitations that would prevent technical advancement -- such as the addition of code-splitting to a project that does not yet have it.
Because the files locations can be fitted to a rule, (i.e: src/${collection}/${namespace}/${name}/${type}
), we can programatically crawl across the project and experiment with 'conventions', or compile scss without importing into the javascript, or invoke some transform on a particular sub-tree of the project.
A more concrete / real-world example (in user-space), by having the files split apart by route, we allow the file system to know our natural route/code-splitting boundaries -- which makes for a much easier implementation of code-splitting.
Implementation
- How do you get everyone on the same page when anything can go?
- How do you achieve consistency between developers?
- How do you remember where something should go?
- How do you manage imports with all these file trees?
For 1 through 3, the only answer for most projects is in-depth code reviews. After the first few established routes, it'll get easier to maintain. But it is inevitably a manual process, as most ecosystems do not have a way to programatically enforce conventions.
For managing imports, the best thing to do is to set up absolute aliases to common entry points.
For example:
"paths": {
"project-name/*: ["."],
"@data/*": ["src/data/*"],
"@models/*": ["src/data/models/*"],
"@ui/*": ["src/ui/*"],
"@components/*": ["src/ui/components/*],
"@env": ["src/env.ts"],
"tests/*": [ "tests/*" ],
"*": ["types/*"],
This does mean that if you have deeply nested components, your import paths may be long, but they are easy to grep
for, and you'll have an easier time moving subtrees around since there are no relative paths to worry about breaking.
An example of a React app implementing most of the criteria outline in this post: Example React App
However, in Ember, there is a resolver. The resolver defines a set of rules for finding things and contextually discovering components, routes, data models, etc. There are a set of conventions that allow the resolver to find things in app-space, so that you don't need to worry about importing them. There is a reference, the resolver looks up the reference, and the thing stubbed in.
Something unique about ember, is that it has a bunch of build-time optimizations that the other ecosystems don't have. This is powered by broccoli, where you can transform parts of your app file tree during the build process. Ember uses this to swap out lookups with the actual reference to a component (for example, could be other things). Broccoli is also used to swap out simple helpers such as {{fa-icon}}
with the rendered html during build so that the bundle can be smaller.
To read more about ember's resolver, feel free to checkout DockYard's article, "Understanding Ember's resolver"
To read more about Broccoli, Oli Griffith has an amazing guide / tutorial on it
An example of this structure can be found here:
emberclear at gitlab (this is the code for emberclear.io, one of my side projects).
The Octane Layout's folder structure satisfies nearly all use cases. And the majority of this post represents a subset of the ideas from the The Octane Layout's RFC.
Note that the Octane layout is not yet released. It's coming early 2019, along with the release Ember Octane
Would I say that this in the layout people should use? maybe. There is some breathing room between what I've outlined for all js ecosystems to use and what the Octane layout dictates for ember-specific apps. Ultimately, if you are in an ecosystem where you have to decide how to lay things out, just keep the guidelines in mind as your placing files around, or copy everything here -- but with some tweaks. Ultimately, you need to do what is best for your team. Personally, with React, I feel close. Maybe there is a tool that could be written for non-ember projects that helps guide structure. Like a linter, but for file locations.
Posted on September 28, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 28, 2018