Hybrid frontend layout: responsibility first, feature second
Refactor (or shape) a frontend so every file has a clear owner. Do not change product behavior.
Source principles: Part 1 (folders), Part 2 (components), Part 3 (logic), Part 4 (API).
Framework names and suffixes: reference.md.
Responsibility first. Feature second.
Do not organize the app as only components/ / hooks/ / services/ with a flat dump of files. Do not organize as only billing/ / users/ with mixed file kinds inside. Combine both.
Shared, cross-cutting code (UI kit, layout, app config, date libs) stays outside feature folders.
Adapt names to the stack; keep this shape:
src/
components/
ui/ # business-agnostic, reusable
layout/ # app shell
transitions/ # animation-only, if the app has them
features/
<feature>/ # owned by one domain
pages/ # or views/ — route-level screens
store/ # or stores/ — shared app state
<feature>/
services/
<feature>/
hooks/ # or composables/
<feature>/ # plus a shared/ folder if truly generic
types/
<feature>.types.ts # or types/<feature>/
lib/ or config/ # third-party setup, env, i18n — not features
Skip empty layers. Do not invent hooks/ or transitions/ if nothing belongs there.
Ask who owns this? before moving anything.
| Kind | Test | Destination |
|---|---|---|
| UI component | Could copy it to another product without changing behavior? Another feature can reuse it with no domain knowledge? | components/ui/ |
| Feature component | Owned by one business feature; may use that feature's store/services/types | components/features/<feature>/ |
| Layout | Still useful if every business feature were removed | components/layout/ |
| Transition | Only animation; no business logic | components/transitions/ |
| Page / view | Route screen | pages/<feature>/ |
| Store | State shared by unrelated parts of the app | store/<feature>/ |
| Service | Talks to an external system (HTTP, storage, third-party SDK) | services/<feature>/ |
| Helper | Pure domain/util logic, no I/O | services/<feature>/*.helper.ts or lib/ if cross-cutting |
| Types | TypeScript contracts | types/ with a feature suffix or folder |
| Hook / composable | Owns one well-defined reactive behavior | hooks/ or composables/, then feature or shared/ |
Do not promote a feature component to ui just because a second feature imports it. Duplicate a small piece or keep it in the owning feature. Promote only after it is truly business-agnostic.
Do not extract a hook because a component got long. Extract only when the logic has its own responsibility. UI-coupled logic stays in the component.
Do not fetch in components or hooks. Hooks may coordinate loading/error/pagination; the request lives in a service. Services must not know about UI, hooks, or stores.
Store vs hook: one component/feature workflow → hook. Unrelated surfaces need the same state → store.
Names should say what the file does before it is opened. Use suffixes on non-component modules:
*.types.ts — contracts*.service.ts — external I/O*.helper.ts — pure helpers*.guard.ts — route/auth guards*.pinia.ts, *.context.tsx, *.store.ts (match the stack)*.vue, PascalCase *.tsx)Copy and track:
- [ ] Inventory src/; list features and current mixed folders
- [ ] Map stack names (Vue/React/other) from reference.md
- [ ] Classify every moved file (table above)
- [ ] Split mixed modules: types vs helper vs service vs store
- [ ] Move files (git mv when tracked)
- [ ] Update imports; switch relative asset imports to the project alias if one exists
- [ ] Remove empty leftover folders and dead barrels
- [ ] Typecheck / lint; fix only breakages from the move
Rules while moving:
modals/, containers/, lib/ catch-alls).components/features/billing/, services/billing/, store/billing/).src/ is responsibilities, not a pile of features or a pile of ungrouped file types