0. What Changed from v1

v1 (Feb 2026) v2
Feature-based architecture Feature-Sliced Design with 4-layer hierarchy
Entities implicit inside features Explicit entities/ layer — single source of truth
No page layer Explicit pages/ layer — route entry points
Workflows for cross-feature orchestration Removed — orchestration is a composing feature
Top-level api/, hooks/, utils/, components/ Consolidated into shared/ and app/
Tool names embedded in principles Principles tool-agnostic; tool stack in §19
No testing strategy Per-layer testing strategy in §15
No component design rules 7 component design principles in §13
No DevEx conventions Import rules, naming, barrel exports in §19
No import hierarchy table Dependency matrix in §5
No performance section Code splitting, bundle budget, prefetch in §18
No accessibility guidelines 7 accessibility directives in §14

1. Architectural Style

  • Feature-Sliced Design with hard boundaries and a strict layer hierarchy.
  • Layers (top → bottom): PagesFeaturesEntitiesShared.
    • A layer may only import from layers below it. No upward or same-layer imports.
  • Pages are route entry points — they own the URL, compose features into a layout, extract params.
  • Features define what users do (actions, mutations, queries on entities) and own their UI.
  • Entities define what things are (data model, business invariants).
  • Shared is reusable code with no business affiliation.
  • Explicit separation of Queries (read-only server state) and Mutations (state-changing operations).
  • Hooks manage server state and orchestration.
  • Components are presentation-only.
  • Single fetch client for all HTTP communication. No ad-hoc fetch calls.
  • Backend errors returned as ProblemDetails must be normalized and surfaced via notifications. No silent failures.
  • All feature calls, queries, mutations, and user-visible errors MUST be traced. No silent paths.

2. Project Structure

src/
├── app/                          # app shell — entry, routing, layouts, global styles
│   ├── routes/                   #   Route definitions, PrivateRoute
│   ├── layouts/                  #   MainLayout, AuthLayout
│   ├── styles/                   #   Theme overrides, global CSS
│   ├── assets/                   #   images, icons, fonts
│   ├── App.tsx
│   ├── main.tsx
│   └── index.css
│
├── shared/                       # reusable code with no business affiliation
│   ├── ui/                       #   Button, Input, Modal, Card
│   ├── api/                      #   fetch client, telemetry abstraction, useApiError, useTrace
│   ├── lib/                      #   date helpers, formatters, validators
│   └── config/                   #   env, constants, feature flags
│
├── entities/                     # business entities — data model & invariants
│   ├── <entity>/
│   │   ├── model.ts              #   types, schemas, invariants, defaults
│   │   ├── api.ts                #   entity-specific API (if needed beyond CRUD)
│   │   ├── ui/                   #   entity cards, avatars, badges, list items
│   │   └── lib/                  #   entity-specific helpers
│
├── pages/                        # route-level page components
│   ├── <page>/
│   │   ├── ui/                   #   page component, page-specific layout
│   │   └── model.ts              #   page-level hooks, route param extraction
│
├── features/                     # user actions on entities, owns UI
│   ├── <feature>/
│   │   ├── api.ts                # semantic API (business intent)
│   │   ├── model.ts              # hooks (queries, mutations), store, types
│   │   └── ui/                   # feature UI
│
└── store/                        # global Zustand stores (cross-feature UI state only)

3. Pages

Route entry points. Map 1:1 to URLs. Own layout composition only.

Contains:

  • ui/ — Page component. Composes features and shared layout. May include page chrome (title bar, breadcrumbs).
  • model.ts — Route param extraction, page-level side effects (scroll-to-top, analytics page-view).

Never contains:

  • Data fetching or React Query hooks (delegate to features).
  • Business logic beyond feature composition.
  • Zustand stores.
  • Direct API calls.
  • Reusable UI (belongs in shared).

Rules:

  1. One page → one route. No shared page components across routes.
  2. Pages compose features and shared UI only. Do not import entities directly.
  3. Page model.ts never contains React Query hooks.
  4. Page is the only layer that knows about the router.
  5. Pages depend on features and shared. Never import from other pages.

Example: pages/dashboard/

pages/dashboard/
├── ui/
│   └── DashboardPage.tsx          # layout: sidebar + feature components
└── model.ts                       # useDashboardParams(), analytics page-view

4. Entities

Domain model layer. Defines what the application works with — independent of how users interact with them.

Contains:

  • model.ts — TypeScript types/interfaces, Zod schemas, business invariants, defaults, computed helpers.
  • api.ts — Entity-level API when CRUD is entity-scoped (e.g., getUser, updateUser used by multiple features).
  • ui/ — Entity UI: avatars, cards, badges, status indicators, list items. Presentation-only, props-driven.
  • lib/ — Entity-specific pure helpers (e.g., fullName(user), isOrderShippable(order)).

Never contains:

  • Mutations or queries representing a user action (belongs in features).
  • Feature-scoped UI: forms, wizards, dashboards.
  • React Query hooks.
  • Zustand stores.
  • Telemetry.

Rules:

  1. Entities depend only on shared/. No importing from features or pages.
  2. Features import entities — never redefine their types or invariants.
  3. Entity model.ts is pure logic only — no side effects, no API calls, no hooks.
  4. When two features need the same entity, the entity is the single source of truth.
  5. Entity ui/ components receive data via props only — never fetch or mutate.

Example: entities/user/

entities/user/
├── model.ts       # User, UserRole, isValidEmail(), DEFAULT_AVATAR
├── api.ts         # getUser(id), updateUser(id, patch)
├── ui/
│   ├── UserAvatar.tsx
│   ├── UserBadge.tsx
│   └── UserListItem.tsx
└── lib/
    └── displayName.ts   # fullName(user): string

5. Import Hierarchy

Strict unidirectional dependency flow. Each layer may only import from layers below it. No upward or circular imports.


Dependency Table:

Layer ↓ / Can import → Pages Features Entities Shared
Pages
Features ⚠️ hooks + types only
Entities
Shared

Rules:

  • Pages → Features: Call feature hooks and render feature UI. Never call api.ts directly.
  • Pages → Entities: Forbidden. Features own entity relationships.
  • Features → Features: Allowed for hooks and types only. Forbidden for ui/ components and api.ts.
  • Features → Entities: Import entity types, API functions, UI components.
  • Entities → Shared: Only shared imports.
  • Shared → Shared: Allowed internally.

Enforcement:

  • Lint with @feature-sliced/steiger or ESLint no-restricted-imports.
  • Any import outside the table must be justified or rejected.

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

Responsibilities (Non-Negotiable):

  • Base URL and headers
  • Auth token injection
  • Correlation ID propagation
  • Request execution
  • Normalize all failures into a single error model
  • Automatic dependency telemetry
  • Never leak raw fetch errors upstream
  • Located at shared/api/client.ts - usable by any layer below pages

Unified Error Model:

All failures are converted into:

ApiError {
  type: 'network' | 'auth' | 'validation' | 'server'
  status?: number
  title: string
  detail?: string
  errors?: Record<string, string[]>
}

ProblemDetails, network failures, CORS issues, and proxy errors must all map here and be logged to Application Insights.


7. Telemetry & Tracing (Mandatory)

Tooling:

  • Azure Application Insights
  • Direct SDK usage is allowed only in shared/api/telemetry.ts

What Must Be Traced:

Event Required Metadata
Query start / failure feature, queryKey
Mutation start / success / failure feature, operationName
HTTP dependency method, url, status
User-visible notification severity, source
Auth / network fatal errors always

No silent paths.


Telemetry Ownership:

  • shared/api/client.ts

    • HTTP dependencies
    • Correlation IDs
  • Feature hooks

    • Semantic intent (loadProfile, updatePassword)
  • shared/api/useApiError

    • User-visible failures only

8. Feature Service Layer (features/<feature>/api.ts)

Semantic boundary, not a transport wrapper.

Rules:

  • Express business intent, not URLs.
  • Normalize backend responses into frontend invariants.
  • Hide backend naming, structure, and quirks.
  • No React Query usage.
  • No side effects.
  • Every exported function defines a stable operation name. Feature hooks consume this name for tracing.

Ownership:

  • One feature owns its services.
  • Other features may consume, never redefine.

9. Queries & Mutations (React Query)

Queries:

  • Read-only server state.
  • Cached globally.
  • Namespaced query keys:
['<feature>', '<resource>', ...]
  • Feature that defines the query owns invalidation.
  • Query lifecycle events are traced.

Mutations:

  • State-changing only.
  • Always user-initiated.
  • Must:

    • Emit telemetry from the mutation hook (start, success, failure).
    • Trigger invalidation.
    • Trigger user-visible notifications via shared/api/useApiError.
    • Telemetry and notification are separate concerns — both fire, neither is optional.

Mutation hooks own side effects and observability.


10. Error Notification Policy (Critical)

Notification Rules:

Context Notification
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) Responsibilities:

  • Decide if error is visible
  • Decide severity (info | warning | error)
  • Format ProblemDetails and validation errors
  • Deduplicate notifications
  • Emit telemetry for all user-visible errors

Hooks must pass explicit context:

{ silent, isBackground, severity, source }

Components never handle errors.

Error Boundaries handle rendering errors; useApiError + notifications handle API errors. Each feature UI must be wrapped in an Error Boundary (see §13).


11. Zustand State Rules (Strict)

Zustand may contain only:

  • UI state (dialogs, steps, filters)
  • Ephemeral cross-feature state

Zustand must never contain:

  • Server entities
  • Lists
  • Cached data
  • Error state
  • Retry counters
  • Telemetry state

React Query is the only source of server state.


12. Components

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

13. Component Design Principles

  1. Extract pure helpers outside the component body. No closure over state.
  2. Use Error Boundaries per feature, not just at the app root. One crash must not break the page.
  3. Split when >1 responsibility. Data fetching, rendering, and validation are separate concerns.
  4. Destructure props. Pass related values as a single object, not individual primitives.
  5. No nested render functions. Extract to a named component.
  6. Use configuration objects for repetitive markup (nav items, filters, table columns). Loop; don’t hardcode.
  7. Assign defaults in destructuring. Not Comp.defaultProps.
  8. Trust the React 19 Compiler. It auto-memoizes. Skip useMemo, useCallback, React.memo unless the Compiler or lint rule flags a specific case.

14. Accessibility

  • Semantic HTML: <nav>, <main>, <section>, <article>, <aside> — not <div> for everything.
  • Icon-only buttons must have aria-label.
  • Form inputs must have associated <label> (htmlFor or wrapping).
  • Focus management: modals trap focus; route changes move focus to the page heading.
  • Images: alt text required — descriptive for content, empty (alt="") for decoration.
  • Color contrast: Mantine theme defaults pass AA. Verify any custom color overrides.
  • Testing: keyboard navigation and screen reader testing included in §15.

15. Testing Strategy

Per-Layer Test Types:

Layer Test Type What to Test Mock
Entities Unit Types, schemas, invariants, pure lib Nothing
Features Integration Hooks → api.ts → shared/api/client HTTP (MSW)
Pages Component / E2E Render with mocked hooks; critical flows Feature hooks, router
Shared Unit Utilities, formatters, config Nothing
Store Unit Zustand state transitions Nothing

Rules:

  • Entities — pure logic only. No mocking. Fastest. Target 100% coverage.
  • Features — test the hook → service → client chain with MSW for HTTP. Verify telemetry events fire, invalidation triggers, notifications on error.
  • Pages — smoke-test rendering with mocked feature hooks. E2E (Playwright) for critical paths: login, checkout, onboarding.
  • Shared — unit-test utilities and config.
  • Store — unit-test state transitions and selectors.

16. Routing (Wouter)

  • One route → one page component in pages/.
  • Route definition files (in app/routes/) map URLs to page components.
  • Page components:

    • Extract route params
    • Pass params to feature hooks
  • Routing layer contains zero business logic.
  • Authentication enforced via PrivateRoute wrapper around pages.

17. Layouts

  • Apply Mantine providers.
  • Configure theme and notifications.
  • Contain no feature logic.

18. Performance

Code Splitting:

  • Route-level: React.lazy() + <Suspense> per page.
  • Feature-level: lazy-load heavy features (chart libs, PDF generators) on user intent (hover, click).

Bundle Awareness:

  • Monitor bundle size per build. Budget: no single chunk > 200 KB uncompressed.
  • Tree-shake aggressively. No library barrel imports.

Prefetch / Preload:

  • Prefetch critical pages on link hover or Intersection Observer.
  • Preload data via queryClient.prefetchQuery — deterministic query keys, zero architectural impact.

Core Web Vitals:

  • Monitor LCP (< 2.5s), INP (< 200ms), CLS (< 0.1).
  • Tools: browser Performance tab, React Profiler, web-vitals library.
  • Architecture helps: code splitting improves LCP; deterministic query keys prevent CLS; Error Boundaries prevent crash-induced layout shifts.

SSR Position:

  • SPA-first. SSR not assumed.
  • Query keys are deterministic. Side effects isolated in mutations.
  • SSR or preloading can be added without structural rewrite.

19. DevEx Conventions

Imports:

  • Absolute imports only. Configure @/ alias to src/. Never use relative paths (../../../).
  • Import order: React/core → libraries → shared → entities → features → pages → styles.

Barrel Exports:

  • Every slice exports a public API via index.ts. Export only hooks and public UI components. Never export internal api.ts or sub-components.

File Naming:

What Convention Example
Components PascalCase OrderList.tsx, UserAvatar.tsx
Hooks camelCase, use prefix useOrders.ts, useApiError.ts
Services / API camelCase api.ts, client.ts
Types / Models camelCase model.ts, types.ts
Utilities camelCase formatDate.ts, validators.ts
Stores camelCase, use prefix useAuthStore.ts

IDE:

  • Extensions: ESLint, Prettier, Error Lens.
  • Format-on-save enabled. Tab size: 2 spaces.

20. Tool Stack

Concern Current Tool Swap Criteria
Server state React Query (TanStack) Equivalent caching, invalidation, mutation API
UI / session state Zustand Simple API, tiny bundle, React-centric
Routing Wouter 2 KB, hook-based, no JSX config
UI components Mantine Theming, notifications, accessibility built-in
Telemetry Azure Application Insights Dependency tracking, correlation IDs
HTTP native fetch via shared/api/client.ts Centralized error normalization, auth injection
Testing Vitest + MSW + Playwright Fast unit/integration, HTTP mocking, E2E

Rules:

  • Direct SDK usage allowed only in shared/api/telemetry.ts.
  • Feature code never references a specific telemetry provider.
  • If a tool is replaced, only shared/ and the relevant adapter change. Features, entities, and pages are unaffected.

21. Mental Model

Page (route entry point)
  │
  └─> Feature Hook (Query / Mutation)
        ├─> Entity (data model, invariants)
        ├─> feature/api.ts (semantic intent)
        ├─> React Query (server state)
        ├─> telemetry (feature-level)
        │
        └─> shared/api/client
              ├─> HTTP
              ├─> dependency telemetry
              └─> ApiError

Import direction (strict): Pages → Features → Entities → Shared. No upward or same-layer imports.


22. Hard Rules (Enforced)

  1. A layer may only import from layers below it. No upward or same-layer imports.
  2. Pages are route entry points — they own layout composition, not data.
  3. Entities define what things are; features define what users do.
  4. Features import entities — never redefine their types or invariants.
  5. Feature services express business intent, not transport.
  6. Hooks are the only layer allowed to call services.
  7. React Query owns all server state.
  8. Zustand never mirrors server data.
  9. Query keys are feature-namespaced.
  10. Only owning feature invalidates its queries.
  11. Mutations always notify; queries notify selectively.
  12. Components never handle errors or orchestration.
  13. Wrap each feature’s UI in an Error Boundary. One component crash must not break the entire page.
  14. No feature call without telemetry.
  15. No user-visible error without an Application Insights trace.
  16. No direct Application Insights SDK usage outside shared/api/telemetry.ts.
  17. Cross-feature orchestration is a feature, not a separate abstraction.
  18. Test at the right layer: entities (unit), features (integration), pages (component/E2E).
  19. Semantic HTML, labeled inputs, focus management, alt text — accessibility is not optional.
  20. If it cannot be traced, it must not exist.
  21. No exceptions.