Architecture v2 - React SPA (Feature-Sliced Design, Entity-Driven, No Workflows)
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): Pages → Features → Entities → Shared.
- 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
fetchcalls. - 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:
- One page → one route. No shared page components across routes.
- Pages compose features and shared UI only. Do not import entities directly.
- Page
model.tsnever contains React Query hooks. - Page is the only layer that knows about the router.
- 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,updateUserused 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:
- Entities depend only on
shared/. No importing from features or pages. - Features import entities — never redefine their types or invariants.
- Entity
model.tsis pure logic only — no side effects, no API calls, no hooks. - When two features need the same entity, the entity is the single source of truth.
- 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.tsdirectly. - Pages → Entities: Forbidden. Features own entity relationships.
- Features → Features: Allowed for hooks and types only. Forbidden for
ui/components andapi.ts. - Features → Entities: Import entity types, API functions, UI components.
- Entities → Shared: Only shared imports.
- Shared → Shared: Allowed internally.
Enforcement:
- Lint with
@feature-sliced/steigeror ESLintno-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
fetcherrors 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)
- Semantic intent (
-
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.
- Emit telemetry from the mutation hook (
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
- Extract pure helpers outside the component body. No closure over state.
- Use Error Boundaries per feature, not just at the app root. One crash must not break the page.
- Split when >1 responsibility. Data fetching, rendering, and validation are separate concerns.
- Destructure props. Pass related values as a single object, not individual primitives.
- No nested render functions. Extract to a named component.
- Use configuration objects for repetitive markup (nav items, filters, table columns). Loop; don’t hardcode.
- Assign defaults in destructuring. Not
Comp.defaultProps. - Trust the React 19 Compiler. It auto-memoizes. Skip
useMemo,useCallback,React.memounless 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>(htmlForor wrapping). - Focus management: modals trap focus; route changes move focus to the page heading.
- Images:
alttext 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
PrivateRoutewrapper 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-vitalslibrary. - 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 tosrc/. 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 internalapi.tsor 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)
- A layer may only import from layers below it. No upward or same-layer imports.
- Pages are route entry points — they own layout composition, not data.
- Entities define what things are; features define what users do.
- Features import entities — never redefine their types or invariants.
- Feature services express business intent, not transport.
- Hooks are the only layer allowed to call services.
- React Query owns all server state.
- Zustand never mirrors server data.
- Query keys are feature-namespaced.
- Only owning feature invalidates its queries.
- Mutations always notify; queries notify selectively.
- Components never handle errors or orchestration.
- Wrap each feature’s UI in an Error Boundary. One component crash must not break the entire page.
- No feature call without telemetry.
- No user-visible error without an Application Insights trace.
- No direct Application Insights SDK usage outside
shared/api/telemetry.ts. - Cross-feature orchestration is a feature, not a separate abstraction.
- Test at the right layer: entities (unit), features (integration), pages (component/E2E).
- Semantic HTML, labeled inputs, focus management, alt text — accessibility is not optional.
- If it cannot be traced, it must not exist.
- No exceptions.