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)

What each file type can and cannot do:

File Types API Calls React Query Zustand Telemetry Errors Renders UI
Entity model.ts ✅ Owns
Entity api.ts ❌ imports ✅ CRUD ❌ client
Entity ui/ ❌ imports ✅ Dumb
Entity lib/ ❌ imports
Feature api.ts ❌ imports ✅ Semantic
Feature model.ts ❌ calls api.ts ✅ Owns ✅ UI state ✅ Trace ✅ ApiError
Feature ui/ ❌ calls hooks ❌ calls hooks
Page model.ts
Page ui/ ❌ calls feature hooks ✅ Layout
shared/api/client ✅ Transport ✅ Dependency ✅ Normalize
shared/ui/ ✅ Generic
shared/lib/
shared/config/
app/routes/ ❌ maps URLs
app/layouts/ ✅ Shell
store/ ✅ UI only

Entity file relationships — all radiate from model.ts; none call each other:

model.ts  ←  api.ts   (imports types, calls client)
model.ts  ←  ui/      (imports types, receives data via props)
model.ts  ←  lib/     (imports types, pure helpers)

Feature file relationships — strict one-way chain, components never skip layers:

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

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.

Why a separate layer. When multiple features need the same type, schema, invariant, or entity UI component, entities provide a single source of truth. Without them, features would either duplicate definitions (drifting invariants) or import from each other (violating the no-same-layer rule in §5). If an entity is consumed by only one feature, question whether it needs its own directory — but keep the types in an entity anyway; they outlive any single feature.

Contains:

  • model.ts — TypeScript types/interfaces, Zod schemas, business invariants, defaults, computed helpers.
  • api.ts — Entity-level CRUD when two or more features share the same data-access function (e.g., getUser, updateUser). If only one feature needs it, move the CRUD into that feature’s own api.ts — entity api.ts earns its place only through reuse.
  • 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

How model.ts, api.ts, ui/, and lib/ connect:

model.ts is the center — it defines the types, schemas, and invariants that everything else in the entity depends on. api.ts imports those types and calls shared/api/client for CRUD. ui/ components receive data shaped by those types via props. lib/ imports types for pure helper signatures. None of the four call each other — they all radiate from model.ts:

model.ts   ←   api.ts   (imports types, calls shared/api/client)
model.ts   ←   ui/      (imports types, receives data via props)
model.ts   ←   lib/     (imports types, pure helpers)

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: Feature model.ts calls entity api.ts; feature ui/ renders entity ui/ components; both use entity model.ts types.
  • Entities → Shared: Entity api.ts uses shared/api/client; entity ui/ uses shared/ui/ components; entity model.ts and lib/ use shared/lib/ helpers.
  • Features → Shared: Feature api.ts uses shared/api/client; feature model.ts uses shared/api/useApiError, useTrace, shared/lib/, and shared/config/; feature ui/ uses shared/ui/ components.
  • 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 api.ts. Only its own model.ts calls it.
  • Other features consume via public hooks — never import api.ts directly.

How api.ts, model.ts, and ui/ connect:

model.ts hooks call functions from api.ts, wrap them in React Query (queries or mutations), and expose the result. ui/ components consume those hooks via props or direct hook calls. Components never touch api.ts directly; api.ts never touches React Query, hooks, or components. The chain is always:

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

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.

Query lifecycle:

Page ui/ ──calls hook──▶ Feature model.ts (useQuery)
                              │
                              ├─ useTrace (start)
                              ├─ Feature api.ts (semantic call)
                              │      └─ shared/api/client (HTTP + dependency telemetry)
                              ├─ useTrace (success | failure)
                              └─ returns data → Feature ui/ → Page ui/

Mutation lifecycle:

Feature ui/ ──onClick──▶ Feature model.ts (useMutation)
                              │
                              ├─ useTrace (start)
                              ├─ Feature api.ts
                              │      └─ shared/api/client
                              ├─ useTrace (success | failure)
                              ├─ on success: queryClient.invalidateQueries([feature, resource])
                              ├─ useApiError (notify + trace on failure)
                              └─ returns result → Feature ui/

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.