Customize Your Application
Find Where Your Change Belongs
Start with the feature you want to change. The project separates application assembly (app/), feature behavior (features/) and shared resources (shared/).
Before continuing, run the application locally and confirm that you can sign in. Make your first change in an existing view before adding a new feature or changing authentication.
| You want to change... | Start here |
|---|---|
| A feature's text, fields or visual arrangement | That feature's components/. |
| Page loading, search or user-action orchestration | Its hooks/use<X>Page.ts. |
| A backend request or response mapping | Its api/, using axiosInstance and apiPath(). |
| A business rule or domain entity | Its domain/. |
| State that one page writes and another page reads | Its store/, if cross-page sharing is needed. |
| The application's routes | app/router/index.tsx, importing the feature through index.ts. |
| A UI component used across features | shared/ui, imported through its public index.ts. |
Make Your First Visible Change
Use src/features/registries/ as the reference feature. The architecture document identifies it as the example for the standard feature structure.
- Open the Registries feature's
index.tsand follow the exported page component intopages/. - In that page, identify the
use<X>Page()hook and the view that receives its returned values. - Find a visible heading or label in the owning view and change its text. Keep API requests and business rules unchanged for this first edit.
- Open the corresponding page in your local application and check the changed text.
Check the result: Your text change appears in the intended view, while its existing data and actions still work.
Understand a Feature's Responsibilities
pages/ -> hooks/ -> api/ -> Backend
| |
| +-> store/ Only when state is shared across pages
+-> components/ Receive data and callbacks as props
The page connects orchestration to presentation. Keep backend response handling and business decisions out of the view.
| Directory | Responsibility | Boundary to keep |
|---|---|---|
domain/ | Entities, constants and business-rule functions. | No React, UI, store or imported API DTO types. |
api/ | HTTP calls, transport DTO types and DTO-to-domain mapping. | No view-layer dependency. |
store/ | State and actions shared across pages. | Optional; a page-specific fetch does not require a store. |
hooks/ | Page orchestration, exposing domain-shaped data and callbacks. | Convert UI events to values in the view before calling a callback. |
components/ | Views receiving data and behavior through props. | No direct API or store calls. Keep presentation helpers beside the view that owns them. |
pages/ | Call the route hook and pass its result to the view. | No local useState / useEffect or data transformation. |
index.ts | The feature's public exports. | Other features and the router use this entry point instead of importing private files. |
For example, a hook can expose onSearchChange(value: string). The view extracts the string from its input event; it does not pass a React or MUI event object into the hook's interface.
If a route has several tabs, keep tab-specific fetches and polling in the tab's hook and container. Data needed by the whole route belongs in the route hook. A tab container can call its hook while the view remains driven by props.
Add a Feature
Once you understand an existing feature, follow this order for a new one:
- Create
src/features/<name>/with the directories needed by the feature. Addstore/only when state must be shared across pages. - Define the core types and business rules in
domain/. - Implement requests in
api/<name>.api.ts. Keep transport DTOs and their conversion at the API boundary. - Add shared state in
store/if needed. Otherwise, let the page hook callapi/directly. - Write a
use<X>Page()hook that returns the data and callbacks the page needs. - Create the views in
components/, then connect the hook and view inpages/. - Export the page and any deliberately shared types or state through the feature's
index.ts. - Register the route in
app/router/index.tsxthrough that public entry point.
Check the result: The new route opens, the view receives the intended data, and its actions work without importing API or store modules into the view.
The project enforces import boundaries through ESLint. shared/ must not import from features/ or app/. Cross-feature imports go through the other feature's index.ts; shared UI imports also use its public entry point.
Keep Styling With Its Owner
Choose the stylesheet location by ownership:
- Application-wide tokens, theme and reset:
app/styles/, loaded byapp/main.tsx. - Reusable component styling: Beside the component in
shared/ui/. - Feature-specific styling: Beside the owning page or component.
SCSS is imported as a side effect, for example import './Example.scss'. It uses global class names rather than CSS Modules, so check for an existing class before introducing one. Do not add new styles to shared/styles/legacy/ or import another feature's private stylesheet.
Use shared UI's semantic tones for status presentation. Keep the meaning of a status in domain/; keep its visual treatment in the UI layer.
Verify the Change
-
Run the project's combined check from its root:
npm run verify -
Open the affected route and exercise the view with the data and permissions used by your example.
-
If you changed data loading, also inspect the loading, empty and error cases relevant to that change.
-
If the route has tabs, switch away and back to check that tab-specific loading and polling follow the intended behavior.
Check the result: Lint, type checking and the build pass, and the changed view behaves as intended in the browser.
Before moving on, check that business rules remain in domain/, DTO mapping remains in api/, and the view can render from its props. For the complete rules and source structure, see ARCHITECTURE.md for v1.0.0.