1. Layers

3 layers. Import only downward.

Layer Purpose Can import
Pages Route entry points, layout composition Features, Shared
Features All business code — data models and user actions Features (barrel only), Shared
Shared Reusable code with no business affiliation Shared only
Pages → Features → Shared
         ↓
   Domain features → Foundation features → Shared

2. Project Structure

src/
├── app/                     # shell: routing, layouts, styles, assets
│   ├── routes/
│   ├── layouts/
│   ├── App.tsx
│   └── main.tsx
│
├── shared/                  # no business affiliation
│   ├── ui/                  #   Button, Input, Modal, Card
│   ├── api/                 #   fetch client, telemetry, useApiError
│   ├── lib/                 #   date, formatters, validators
│   └── config/              #   env, constants, feature flags
│
├── pages/                   # 1:1 with routes
│   └── <page>/
│       ├── ui/              #   page component
│       └── model.ts         #   route params, page-level side effects
│
├── features/                # all business code
│   ├── user/                #   foundation feature (data model)
│   │   ├── model.ts         #     types, schemas, hooks
│   │   ├── api.ts           #     CRUD (internal)
│   │   ├── ui/              #     UserAvatar, UserBadge
│   │   ├── lib/             #     pure helpers
│   │   └── index.ts         #     barrel (public contract)
│   │
│   ├── auth/                #   domain feature (user action)
│   │   ├── model.ts
│   │   ├── api.ts
│   │   ├── ui/
│   │   └── index.ts
│   │
│   └── checkout/            #   domain feature
│       ├── model.ts
│       ├── api.ts
│       ├── ui/
│       └── index.ts

No top-level store/ directory. All Zustand stores live in features.


3. Feature Internal Structure

Every feature has the same shape. No distinction between “entity” and “business” features in folder structure.

File Purpose Public?
model.ts Types, schemas, invariants, hooks (useQuery, useMutation). Owns React Query. Owns telemetry for its operations. Owns feature-local Zustand stores (UI state only). Yes — exported in barrel
api.ts Semantic API calls (business intent, not URLs). No React Query. No side effects. No — internal only
ui/ Feature UI components. Presentation-only, props or hook driven. Yes — exported in barrel
lib/ Pure helpers, no side effects. Yes — exported in barrel
index.ts Barrel export. The feature’s public contract.

Feature UI state (e.g., “is login dialog open”) lives as a Zustand store defined in model.ts. Cross-feature UI state lives in the feature whose domain owns it, exported via barrel for other features to consume.

Internal chain:

ui/ → model.ts → api.ts → shared/api/client → HTTP

4. Barrel Contract

index.ts is the feature’s public API. Other features import from @/features/<name> — never from internal paths.

Exported: types, hooks, lib functions, public UI components. Not exported: api.ts functions, internal sub-components, internal hooks.


5. Feature Types

Two conceptual roles. Same structure. Different dependency rules.

Foundation features — data models (“what things are”). Naming heuristic: nouns (user, order, product). Not a hard rule.

  • Depend only on shared/. Never on other features.
  • Own the types. Other features consume, never duplicate.
  • api.ts has CRUD. Exposed only via hooks in barrel.
  • Export entity UI components (avatars, badges, cards) in the barrel for cross-feature reuse.

Domain features — user actions (“what users do”). Naming heuristic: domain actions (auth, checkout, dashboard). Not a hard rule.

  • Depend on shared/ + foundation features (via barrel).
  • Import types, hooks, lib, and UI components from foundation features.
  • Never import from other domain features. If two domain features need shared logic, extract it to a foundation feature.
  • Never import api.ts from any feature.

Extract to a foundation feature when a type or hook is needed by 2 or more domain features.


6. Cross-Feature Import Rules

Import Domain → Foundation Domain → Domain Foundation → any feature
Types (from barrel)
Hooks (from barrel)
Lib functions (from barrel)
UI components (from barrel)
api.ts
Internal paths (@/features/x/api)

Domain features import from foundation features only. Domain→domain imports are forbidden — extract shared logic to a foundation feature. Foundation features depend on shared/ only, never on other features.

Lint enforcement: ESLint no-restricted-imports blocking @/features/*/api and cross-domain imports.


7. Pages

Route entry points. 1:1 with URLs.

Contains: page UI component, route param extraction, page-level side effects (scroll-to-top, analytics page-view). Never contains: data fetching, React Query, Zustand, API calls, business logic, reusable UI.

Pages compose features and shared UI. Never import from other pages.


8. API Client (shared/api/client.ts)

Single fetch wrapper. Responsibilities: base URL, auth token injection, correlation ID, request execution, normalize all failures into ApiError, dependency telemetry. Never leaks raw errors.

ApiError: { type: 'network'|'auth'|'validation'|'server', status?, title, detail?, errors? }


9. Telemetry

Azure Application Insights. SDK usage only in shared/api/telemetry.ts.

Must trace: query start/failure, mutation start/success/failure, HTTP dependencies, user-visible notifications, auth/network fatal errors. No silent paths.

Ownership: Feature hooks own semantic-intent tracing (useTrace calls). shared/api/client owns HTTP dependency telemetry. useApiError owns user-visible error tracing.


10. React Query

Queries: read-only. Cached globally. Key format: ['<feature>', '<resource>', ...]. Owning feature manages invalidation.

Mutations: state-changing. User-initiated. Must emit telemetry (start, success, failure), invalidate queries, notify on error via useApiError. Both telemetry and notification fire — neither is optional.

Cross-feature invalidation: When a mutation in one feature affects another feature’s queries, the owning feature exports an invalidation hook in the barrel (e.g., useInvalidateUser). The mutating feature calls that hook. Never reach into another feature’s queryClient calls directly.


11. Error Notifications

Context Notify?
Mutation error Always
Query — initial blocking load Yes
Query — background refetch No
Query — polling No
Network / auth fatal errors Yes

useApiError (shared/api/useApiError.ts): decides visibility, severity, formats ProblemDetails, deduplicates, traces user-visible errors. Components never handle errors.


12. Zustand

Only: UI state (dialogs, steps, filters), ephemeral cross-feature state. Never: server entities, lists, cached data, error state, retry counters, telemetry state.

All Zustand stores live in features — in model.ts, exported via barrel if cross-feature. No top-level store/ directory. React Query is the only source of server state.


13. Components

Presentation-only. Receive data via hooks or props. No API calls, no mutations, no orchestration, no error handling, no telemetry. Display loading/empty/success states.

Per-feature Error Boundaries. One crash must not break the page.


14. Component Design

  1. Pure helpers outside component body. No closure over state.
  2. Error Boundaries per feature, not only at root.
  3. Split when >1 responsibility.
  4. Destructure props. Related values as single object.
  5. No nested render functions. Extract to named component.
  6. Configuration objects for repetitive markup. Loop, don’t hardcode.
  7. Defaults in destructuring, not Comp.defaultProps.
  8. Trust React 19 Compiler. Skip useMemo/useCallback/React.memo unless flagged.

15. Accessibility

Semantic HTML (<nav>, <main>, <section>, <article>, <aside>). Icon-only buttons: aria-label. Form inputs: associated <label>. Focus: modals trap, route changes move to heading. Images: alt required. Mantine defaults pass AA; verify overrides.


16. Testing

Layer Type What Mock
Features Integration + unit Hook → api → client chain; pure types/lib; Zustand state transitions HTTP (MSW); nothing for pure
Pages Component / E2E Render with mocked hooks; critical flows Feature hooks, router
Shared Unit Utilities, formatters, config Nothing

17. Routing

Wouter. One route → one page. Routes in app/routes/ map URLs to pages. Zero business logic in routing. Auth via PrivateRoute wrapper.


18. Performance

Code splitting: route-level React.lazy() + <Suspense>; feature-level lazy load on user intent. Bundle budget: no chunk > 200 KB uncompressed. Prefetch via queryClient.prefetchQuery or link hover. Monitor LCP/INP/CLS. SPA-first; SSR not assumed but achievable.


19. DevEx

Absolute imports only (@/ alias). Import order: React → libs → shared → features → pages → styles. Barrel exports per feature. File naming: components PascalCase, hooks useCamelCase with use prefix, helpers camelCase. Fixed filenames: api.ts, model.ts, index.ts. 2-space tabs. Format on save.


20. Tool Stack

Concern Tool
Server state React Query (TanStack)
UI state Zustand
Routing Wouter
UI components Mantine
Telemetry Azure Application Insights
HTTP native fetch via shared/api/client.ts
Testing Vitest + MSW + Playwright

Replacements only affect shared/. Features and pages unaffected.


21. Hard Rules

  1. Import only downward: Pages → Features → Shared.
  2. Pages own layout composition, not data.
  3. All business code in features. No separate entity concept.
  4. Foundation features depend only on shared. Domain features depend on shared + foundation. Domain→domain imports forbidden.
  5. api.ts always internal. Cross-feature consumption via barrel (types, hooks, lib, UI) only.
  6. Hooks are the only layer allowed to call services.
  7. React Query owns all server state.
  8. Zustand never mirrors server data. All Zustand stores live in features (model.ts), exported via barrel if cross-feature. No top-level store/.
  9. Query keys feature-namespaced. Only owning feature invalidates. Cross-feature invalidation via exported invalidation hooks.
  10. Mutations always notify; queries selectively.
  11. Components never handle errors or orchestration.
  12. Error Boundary per feature UI.
  13. No feature call without telemetry. No user-visible error without trace.
  14. Application Insights SDK only in shared/api/telemetry.ts.
  15. Every feature has a barrel (index.ts). Cross-feature imports go through barrels, never internal paths.
  16. Cross-feature imports: types, hooks, lib, UI — never api.ts. Domain features import from foundation features only.
  17. Foundation features never depend on other features.
  18. Semantic HTML and accessibility not optional.
  19. If it cannot be traced, it must not exist.
  20. No exceptions.