Skip to main content

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 arrangementThat feature's components/.
Page loading, search or user-action orchestrationIts hooks/use<X>Page.ts.
A backend request or response mappingIts api/, using axiosInstance and apiPath().
A business rule or domain entityIts domain/.
State that one page writes and another page readsIts store/, if cross-page sharing is needed.
The application's routesapp/router/index.tsx, importing the feature through index.ts.
A UI component used across featuresshared/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.

  1. Open the Registries feature's index.ts and follow the exported page component into pages/.
  2. In that page, identify the use<X>Page() hook and the view that receives its returned values.
  3. 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.
  4. 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.

DirectoryResponsibilityBoundary 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.tsThe 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:

  1. Create src/features/<name>/ with the directories needed by the feature. Add store/ only when state must be shared across pages.
  2. Define the core types and business rules in domain/.
  3. Implement requests in api/<name>.api.ts. Keep transport DTOs and their conversion at the API boundary.
  4. Add shared state in store/ if needed. Otherwise, let the page hook call api/ directly.
  5. Write a use<X>Page() hook that returns the data and callbacks the page needs.
  6. Create the views in components/, then connect the hook and view in pages/.
  7. Export the page and any deliberately shared types or state through the feature's index.ts.
  8. Register the route in app/router/index.tsx through 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 by app/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​

  1. Run the project's combined check from its root:

    npm run verify
  2. Open the affected route and exercise the view with the data and permissions used by your example.

  3. If you changed data loading, also inspect the loading, empty and error cases relevant to that change.

  4. 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.