I was looking for a component called StatusBadge a while back, and the search turned up four different import paths pointing at the same file. One went through a relative path with three levels of ../ in it. One imported it from a barrel file two directories up. One went directly at the file. One came through a re-export in a completely unrelated feature that had apparently needed it once and exported it again on the way through.
Every one of those imports worked. The bundler resolved all four, TypeScript was happy, nothing was broken in any way a test would catch. But if you wanted to move that file, you now had four different kinds of reference to fix, and no reliable way to know you had found all of them.
This is part two of a series on writing a rule file for React. Part one covered state. This one covers the structural layer, because the shape of your folders decides how much any of your other rules can actually accomplish.
The two structures and why the choice matters more now
The first structure is organising by type. Components in a components folder, hooks in a hooks folder, services in a services folder, types in a types folder. It is the structure most tutorials use and the one most projects start with, because at twelve files it is obviously correct and requires no thinking at all.
The second is organising by feature. Every feature owns a folder, and inside that folder sit its components, its hooks, its services, its types. Shared code exists, but only for things genuinely used across multiple features.
Before AI was writing a meaningful share of the code, this was mostly a preference argument. Both structures work. Plenty of successful projects use each one. The type-based version gets awkward around a hundred files, the feature-based version has more upfront ceremony, and teams picked whichever fit how they thought about the product.
What changed is that the AI has to place every new file it creates, and it makes that decision from whatever it can see in the immediate context of that one session. In a type-based structure there is very little in the context that says where the new hook belongs, because the hooks folder holds forty unrelated hooks and none of them tell you anything about this one. In a feature-based structure the answer is usually sitting right there in the same folder as the thing being extended.
What the type-based structure does across enough sessions
The components folder grows without any grouping, so after a few months you have eighty files in one directory and finding anything means knowing the exact name in advance.
Imports start reaching sideways because there is no boundary saying they should not. A component in one part of the product imports a hook written for a different part, because the hook is sitting in the same hooks folder and looks applicable. That works fine until someone changes the hook for its original purpose and breaks something three features away that nobody remembered was connected.
Relative paths get deep. When related files sit in four different top-level folders, the path between them climbs up and back down, and the AI writes whatever path resolves. Nothing in a type-based layout discourages ../../../, so it accumulates.
The part that surprised me most was how much it undermines the state rules from part one. Rule one says state moves to a hook in the feature when two components in that feature need it. That rule assumes a feature is a place you can point at. In a type-based structure it is a concept in your head, so the AI reads the rule, has no folder to put the hook in, and drops it into the general hooks folder alongside everything else, where the next feature will eventually import it and the boundary the rule was protecting stops existing.
The architecture section
Five rules. Same file as part one, section below it.
## Architecture and Files
Structure is feature based. Each feature folder contains its
own components, hooks, services and types. Technical folders
are allowed only for building blocks genuinely used across
multiple features.
Every feature exposes a public API through an index file.
Imports from outside the feature go through that file and
nothing else. Files not exported from the index are private
to the feature.
No deep relative paths. If an import needs more than one
level of ../ it is reaching across a boundary that should
have gone through a public API.
Layer order is UI, then hooks, then services, then the API
layer. Each layer talks to the one below it. A component
never calls the API layer directly and never knows what
shape the backend returns.
One responsibility per file. Files above roughly 200 to 300
lines get split before any further work continues, and a
refactor is not finished until the imports are consolidated.
Going through them
Rule one is doing something subtle that took me a while to notice. It gives every future file a home that can be worked out from the thing being extended. When the AI adds a hook for a feature, the folder is already sitting next to the component it belongs to. The decision stops being open.
The sentence about technical folders is the escape hatch, and it needs to be in there or people start putting genuinely shared things into feature folders and importing across boundaries to reach them. A date formatter used by six features belongs in a shared folder. A hook used by one feature does not, even if someone can imagine a second feature wanting it later.
Rule two is the one that actually holds the structure together, and it is the one people skip because it feels like extra work for no return. The return shows up the first time you refactor the inside of a feature. Everything not in the index file is yours to move, rename or delete, because you can see with certainty that nothing outside the folder depends on it. Without that guarantee every internal change is a search across the whole codebase.
It also gives the AI a much clearer signal about what is available. When a session looks at a feature from the outside, the index file is a short list of things meant to be used. The alternative is the AI scanning a folder of internal files and picking whichever one looks closest to what the prompt described, which is how a private helper ends up imported into three other places.
Rule three is the smallest one and I almost folded it into rule two. It stayed separate because it is the one you can check in about four seconds. Deep relative paths are the visible symptom of a boundary being crossed, so a rule that names the symptom directly gets applied more reliably than a rule that only describes the underlying principle. Search your project for ../../../ and whatever comes back is a list of places where the structure is not holding.
Rule four is the layer order, and the last sentence carries most of the value. A component that knows the backend returns user_id and full_name is a component that breaks when the backend renames a field. Once the mapping lives in the service layer, an API change touches one function. I have watched a field rename turn into a fourteen file pull request because the raw shape had leaked upward into components over about a year, and none of those fourteen changes were interesting work.
Rule five is the file size limit, and the range matters more than the exact number. Two hundred is not meaningfully different from two hundred and fifty. What matters is that there is a point where splitting stops being optional, because "this file is getting long" is not something a session can evaluate consistently and a line count is. The clause about the refactor being unfinished until imports are consolidated is there because half-done splits are worse than no split. You end up with the new files and the old paths still pointing at the old locations through compatibility re-exports that nobody removes.
The migration question
If you are sitting on a type-based structure with a few hundred files, moving everything is not realistic and I would not suggest it.
What worked for me was applying the rules to new features only, and moving an existing one when it needed real work anyway. The next feature gets a folder with the structure inside it. The one after that too. Anything already there stays where it is until there is a reason to touch it.
That leaves you with a mixed codebase for a while, which feels wrong and is fine. The alternative is a migration that takes three weeks, produces no user-visible change, and stalls halfway through when something more urgent comes up.
Part three covers typing and data contracts, which is where the layer rule from this section either holds or quietly falls apart.
Top comments (0)