Recommended resources
The freely available resources this lesson will direct you to.
Welcome to the maintainable-project architecture module. In the last lesson, you added an Escape-key listener to the directory and cleaned it up when the selection or component changed. The behavior works, but the page is accumulating responsibilities: it renders records, manages interactions, and sits close to application setup.
This lesson gives those responsibilities distinct homes. You’ll organize the existing React code into app, page, feature, entity, and shared layers, using the user directory as your example. The aim is not to create a large folder tree; it is to make ownership and allowed dependencies clear as the back-office application grows. Allow about 40 minutes to study the model and move the current code.
Organize by responsibility, not file type
A project-wide components folder can tell you that a file renders UI, but not whether it represents a user record, implements a search interaction, or assembles an entire screen. A project-wide hooks folder has the same problem. Layers answer a different question: how broad is this code’s responsibility?
Feature-Sliced Design uses layers for that broad responsibility, slices for distinct business areas within a layer, and segments such as ui or model to arrange files inside a slice. Watch this short explanation for the vocabulary before applying it to the directory.
Is Feature Sliced Design the BEST Architecture for Frontend Apps?
Watch “Is Feature Sliced Design the BEST Architecture for Frontend Apps?” from CodeFoundry for a concise introduction to layers, slices, and segments.
Watch the three concepts. Notice that user can name a business slice, while ui and model describe different kinds of work within that slice.
The image below shows all six layers in the full model. This course will use five to begin with: widgets are optional. They become useful for large, self-contained UI blocks, but the current directory does not need one.

The order also governs imports. Code in a layer may import from a strictly lower layer, not a higher one. Different slices on the same layer do not import from one another by default, although files within a slice can work together. app and shared are exceptions to the slice rule: they usually contain segments directly rather than business slices.
Read the authoritative layer reference for that rule and for the distinction between shared infrastructure, a business concept, and an interaction.
Layers | Feature-Sliced Design
Read the Feature-Sliced Design reference to establish both the import rule and the responsibilities of the lower layers.
In “Import rule on layers,” read the import rule, including its example of two feature slices. Then, under “Layer definitions,” read “Shared” and “Entities” in the lower layers. Finally, read “Features,” from the interaction guidance. Focus especially on why not every button or local interaction needs its own feature slice.
For this application, a User record is a business concept: code that defines or presents users can eventually live in entities/user. Searching the directory is an interaction, but that does not automatically make it a features/user-search slice. While search is used only on one screen and remains easy to navigate there, keeping it in that page is simpler. A generic UI control with no knowledge of users belongs in shared; a control whose text and behavior specifically select users does not become shared merely because it is a component.
Give the screen and the application different jobs
The page owns a screen. It can assemble smaller parts, keep state used only on that screen, and contain UI that is not reused elsewhere. The app owns application-wide setup: the root component, providers, global styles, and, when added later, route configuration. A page should not need to import the application root to do its job.
Continue in the same reference for the top of the hierarchy. It also describes widgets; you can read that short section for context without creating a widgets folder now.
Layers | Feature-Sliced Design
Continue the reference to decide what remains on the directory screen and what belongs at the application root.
Under “Widgets,” read from widgets versus pages. Under “Pages,” read page ownership. Then skip the deprecated “Processes” section and, under “App,” read app-wide ownership and its short list of typical segments.
This gives the existing Escape shortcut a natural home. SelectionShortcut is specific to the directory’s selection and currently used on just that screen, so keep it inside the directory page slice. Its useEffect and cleanup do not change when you move the file. Later, if a substantial selection interaction is needed across several screens, you can consider a feature slice with a deliberate public interface.
The important distinction is between where code is used and what it owns. For example, the Vite entrypoint may remain src/main.tsx because that is how this project starts; conceptually it performs app setup. The directory’s search text and selected ID, by contrast, belong with the screen that uses them, not in app simply because they were previously inside a component called App.
Move the existing directory without redesigning it
First make only the folders needed for code you already have:
src/
main.tsx
app/
App.tsx
pages/
user-directory/
index.ts
ui/
UserDirectoryPage.tsx
SelectionShortcut.tsx
Here, user-directory is a page slice and ui is a segment within it. There are no empty features, entities, or shared directories yet. Add those when code has a reason to live there.
Make the move in small steps:
- Move the current directory implementation from
src/App.tsxtosrc/pages/user-directory/ui/UserDirectoryPage.tsx. Rename its exported component fromApptoUserDirectoryPage. Keep its current search, selection, derived values, and display logic together for now. - Move the
SelectionShortcutcomponent from that file tosrc/pages/user-directory/ui/SelectionShortcut.tsx. Export it from its new file and import it intoUserDirectoryPage.tsxusing./SelectionShortcut. Keep its props, Effect, dependency array, and cleanup unchanged. If the page no longer usesuseEffectitself, remove that import from the page. - Update any relative imports affected by the move. For instance, an import of
./App.cssfrom the oldsrc/App.tsxlocation will no longer point to the same file. Keep the stylesheet where it is for this refactor and correct its path, or move a genuinely global stylesheet into app setup.
Give the page slice one entry point:
// src/pages/user-directory/index.ts
export { default as UserDirectoryPage } from './ui/UserDirectoryPage'
Now app can assemble the screen without reaching into its private ui folder:
// src/app/App.tsx
import { UserDirectoryPage } from '../pages/user-directory'
export default function App() {
return <UserDirectoryPage />
}
Finally, change the App import in src/main.tsx to ./app/App; leave its rendering setup and any other imports in place. The page’s index.ts is a public entry point: outside code imports what the page slice chooses to expose, rather than depending on the location of UserDirectoryPage.tsx within it. Files inside user-directory can still import one another directly.
This refactor should change ownership, not behavior. If the shortcut stops clearing a visible selection—or still clears one after you turn it off—look for a component or import changed during the move before rewriting the Effect.
Decide where new code will go
The other three layers become useful as the directory expands. Use the responsibility of the code, not its filename extension, to decide where it belongs.
| Layer | Question it answers | Possible directory code |
|---|---|---|
entities |
What business concept does the app work with? | A shared User type or a basic user representation in entities/user |
features |
What significant user interaction is implemented here? | A user-editing interaction used in more than one screen |
shared |
What is reusable without knowing this business domain? | A generic UI primitive or focused formatting library |
A user-specific function does not belong in shared just because several files call it. Conversely, do not copy a User type into every page once multiple screens need to agree on its shape; give that concept a single owner in entities/user. When you introduce a feature that uses users, it may import the user entity. The user entity must not import the feature: that would make the underlying business concept depend on one particular interaction.
Similarly, pages/user-directory may import a feature or an entity, but it should not import implementation files from a different page slice. If two pages need the same behavior, decide whether it is an entity concern, a feature, or genuinely shared infrastructure rather than coupling the pages together.
Avoid promoting code preemptively. The current search and SelectionShortcut can stay in pages/user-directory until a concrete second use or an unwieldy page gives you a reason to extract them. This keeps the structure useful for finding code rather than turning it into a collection of mostly empty categories.
Once the move is complete, run the checks you used in the foundations module:
npm run typecheck
npm run build
npm run dev
In the running app, check that searching still changes the displayed result, selecting a user still updates the selection, and Escape works only while the shortcut is active for a visible selection. These checks tell you whether the architectural change preserved the behavior you already established.
What to carry forward
Pages own screens; app owns application-wide assembly. Entities own business concepts, features own substantial interactions, and shared owns code that is not tied to a business domain. Imports follow those responsibilities downward, with each page or business slice exposing only what outside code needs. For now, the most valuable architecture is a small one that accurately describes the code you have.
Next, you’ll centralize Material UI palette, typography, and component defaults. The distinction you made here will help keep that app-wide theme setup separate from the user-directory screen.