I’ve seen a lot of different ways of organizing source code files and folders over the past two decades or so. Along the way, I picked up a few patterns that have worked very well for me. I continue to improve the system, but, frankly, the improvements have become less frequent and smaller.
I think I might be nearing perfection! Heh.
The file is named “index”
One thing that I adopted very early on—so early that it was back when all my files ended with .html
—is to name the default file in the folder “index
”.
The immediate benefit of this back in the day, and I can’t believe I’m admitting this publicly, is that if I wanted to add superpowers to the file, I could simply rename index.html
to, cough, cough, index.php
. And it just worked.
We can just pretend that I never said that. But it worked just as well later when it became index.rb
and then index.scala
and finally index.ts.
Wait, what? Then how does one name the components if every file is called index
?
The name of the component, utility function, whatever goes on the folder, naturally:
App/
index.ts
index.module.css
index.test.ts
Benefit one: none of this nonsense:
import App from "./App/App"
It’s just:
import App from "./App"
The other great benefit of this is that we can colocate all the files that relate to that component/function in the same folder.
That means we don’t have a separate tests
or __tests__
folder somewhere far from our component. Or a stories
folder.
It means that I can tell at a glance if this component has CSS associated with it, is tested, has Storybook stories, etc. Because the files are right there.
It’s all right there in the folder!
So that’s the key to all this:
Keep things that belong together… together. Who knew?
Everyone wants to belong
Another benefit of this is that I can close up those folders, so instead of seeing this monstrosity:
We can has this:
And that works all the way down:
Mirror the application structure
It’s the application, stupid.
If your interface is represented by components, and those components are nested, then why wouldn’t you nest them in your source code folder hierarchy?
Consider this: if the BreadcrumbTrail
, Logo
, and a given Nav
component appear only in the Header, then shouldn’t their code be nested there as well?
If I moved the Nav
component up to be a sibling of Header
and did the same in with the footer’s Nav
, then I’d have to rename them HeaderNav
and FooterNav
. It’s just more noise. Nesting them makes it clear.
Sharing components or functions
But what if it’s the same Nav
component? Maybe we’ve abstracted it.
OK, then, we only need one, but where does it go?
In this instance, the folder for that component or function should be raised to the node in the tree where the branches that use the component meet. So in this instance, if we were going to make those two Nav
components into one, we’d take this:
And turn it into this:
The Nav
component is used by both Footer
and Header
. These two branches meet at the App
node, so the shared
folder goes directly beneath the App
node.
If, for example, FinePrint
and Social
shared a component, then it would go in a shared
folder directly under the Footer
folder, as that’s the lowest point still above the two branches where the component is used.
Or if Logo
and Social
shared a component, then that component would have to go in the shared
folder under App
, because it is used in both the Footer
and Header
branches and App
is where they meet.
Got it?
It’s pretty simple. And now whenever you see a shared
folder, you know:
The items in this folder are used in more than one place.
Wherever those places are, they are guaranteed to be below the
shared
folder’s parent folder. That is, they are in siblings of theshared
folder.
Utilities?
I generally use shared
folders for shared components. For utility functions, I have a separate shared folder called utilities
. This separation of concerns improves readability in the shared folders. I’m not visually sorting through functions and components trying to find what I want.
Which brings up a thumb rule: If the folder name is PascalCase, then it probably belongs in the shared
folder; if the folder name is camelCase, then it probably belongs in the utilities
folder.
Utility functions follow the same principle as shared components: they go as low in the folder hierarchy as they can. Which means that if a utility function is used by only one component (or other utility function), then it belongs in the folder of that component:
The utilities
folder under main holds utilities used only by Main
and maybe below. If a utility in this folder is needed in, for example, the Header
, then that utility’s folder would be moved into a utilities
folder immediately under App
.
There is almost always a significant number of utility functions that I use pretty much everywhere. If they are not specific to this application, I generally put them into a top-level folder, sibling to App
:
I’m not a fan of adding dependencies willy-nilly. So rather than add, say, Ramda or Lodash (I prefer the former), I simply write my own utilities. It’s not difficult, and then I know not only that they work, but how they work. If it’s something difficult or there is a performance issue, then OK, maybe it’s dependency time, e.g., I’m not writing my own version of React… or am I?
Thus functions such as identity
, noOp
, and pipe
.
But the point of this top-level utilities folder is that I could easily extract it into an npm or Deno module. I’m particularly fond of Deno modules these days. Nothing in this utilities
folder is specific to my app. Nothing.
Services
Ditto for the services
folder. These are generic services my app uses. Nothing specific to my app lives in the services
folder. I inject that information where I use the service in my app (in the App
folder).
That means that if I’m using a specific URL to connect to the database (isn’t everyone?), then that URL will be assigned to a constant somewhere in a constants.ts
file, and that file will be in a utilities
folder at the lowest point practicable in the App hierarchy, where everything specific to this app belongs.
As with the top-level utilities
folder, each of these services could be extracted into an npm or Deno module and loaded from the net. Generally, that’s the plan, and they are only colocated here during development.
One benefit of Deno is that we can load them directly from, say, GitHub while keeping them in this folder for easy reference. But then I guess we could also publish them to npm directly from here. It’s just a bit messier.
What are the benefits of this style?
Other than clarity and reduced cognitive load, which are enough to justify the price of admission in my book, the self-contained component/app/function lends itself to easy movement, duplication, or deletion. Consider this:
If I decide to delete the Sidebar
, I just delete its folder. I don’t need to hunt around for tests, stories, CSS specific to the component, or subcomponents. I just delete the Sidebar
folder and delete any references to the Sidebar
, which are almost certainly only in the Main
component.
So easy and clean!
And what if I decide that the Footer and the Header should really be subcomponents of Main? I just drag the folders in there and I’m probably done:
Again, easy as can be. And everything travelled with these components: their authentication, their connections to the database or to some kind of state, other service connections such as pubsub
.
Or maybe I want to make the Sidebar
a sibling of Footer
, Header
, and Main
:
Hard to beat. Questions? I’ll probably touch on this again soon, maybe when I discuss how I split code up into files and use the module system for easy and clean reuse.