@synergyerp/frontend-standards 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2942 @@
1
+ # Frontend Standardization & Deployment Guide
2
+
3
+ > **Purpose**: Company-wide standards for frontend project architecture, code quality, CI/CD, and code reviews.
4
+ > **Target Audience**: All frontend engineers, QA engineers, and DevOps teams.
5
+ > **Date**: June 2026
6
+ > **Status**: v1.0 -- Standard
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ 1. [Philosophy & Guiding Principles](#1-philosophy--guiding-principles)
13
+ 2. [Recommended Tech Stack](#2-recommended-tech-stack)
14
+ 3. [Project Scaffolding & Directory Structure](#3-project-scaffolding--directory-structure)
15
+ 4. [TypeScript Configuration Standards](#4-typescript-configuration-standards)
16
+ 5. [Code Quality & Linting Standards](#5-code-quality--linting-standards)
17
+ 6. [Formatting Standards](#6-formatting-standards)
18
+ 7. [UI Component Standards (Lovable Integration)](#7-ui-component-standards-lovable-integration)
19
+ 8. [Testing Standards](#8-testing-standards)
20
+ 9. [Git Workflow & Commit Standards](#9-git-workflow--commit-standards)
21
+ 10. [Code Review Standards](#10-code-review-standards)
22
+ 11. [CI/CD Pipeline Standards](#11-cicd-pipeline-standards)
23
+ 12. [Deployment Standards](#12-deployment-standards)
24
+ 13. [Security Standards](#13-security-standards)
25
+ 14. [Monitoring & Observability](#14-monitoring--observability)
26
+ 15. [Documentation Standards](#15-documentation-standards)
27
+ 16. [Appendix: Quick References](#16-appendix-quick-references)
28
+ 17. [API Service Layer Standards](#17-api-service-layer-standards)
29
+ 18. [React Hooks Best Practices](#18-react-hooks-best-practices)
30
+ 19. [Enforcement: Husky, Git Hooks & Automated Validation](#19-enforcement-husky-git-hooks--automated-validation)
31
+
32
+ ---
33
+
34
+ ## 1. Philosophy & Guiding Principles
35
+
36
+ ### 1.1 Core Tenets
37
+
38
+ 1. **Consistency Over Configuration**: Every project should look, feel, and behave the same way so engineers can move between projects seamlessly.
39
+ 2. **Automation Over Manual Processes**: Linting, formatting, testing, and deployments must be automated. Human judgement is reserved for architecture and code reviews.
40
+ 3. **Security By Default**: Security checks, input validation, and dependency auditing are non-negotiable gates in every pipeline.
41
+ 4. **Documentation As Code**: Key decisions live as close to the code as possible (READMEs, ADRs, inline JSDoc).
42
+ 5. **Accessibility First**: Every UI component must be accessible (WCAG 2.1 AA minimum).
43
+
44
+ ### 1.2 Separation Of Concerns
45
+
46
+ This document is split into two distinct parts:
47
+
48
+ | Part | Focus | Audience |
49
+ |------|-------|----------|
50
+ | **Frontend Standards** (Sections 2-9) | Code structure, linting, UI components, testing, git | Developers, Tech Leads |
51
+ | **CI/CD & Deployment** (Sections 10-14) | Pipelines, code review, deployment, monitoring, security | DevOps, QA, Tech Leads |
52
+
53
+ ---
54
+
55
+ ## 2. Recommended Tech Stack
56
+
57
+ ### 2.1 Framework & Runtime
58
+
59
+ | Layer | Recommended | Alternative | Notes |
60
+ |-------|------------|-------------|-------|
61
+ | **Framework** | React 19 + Vite | Next.js(if SSR/SEO needed) | Choose based on project needs. Lovable generates React components natively. |
62
+ | **Language** | TypeScript 5.5+ (strict mode) | -- | Non-negotiable. No plain JavaScript in new projects. |
63
+ | **Build Tool** | Vite 6 | Turbopack (Next.js only) | Fastest build times. |
64
+ | **Package Manager** | Any (pnpm 9+ preferred) | npm 10+, yarn, bun | Developers may use any manager locally; however, **pnpm** is preferred for CI/CD and deployment for performance and consistency. |
65
+
66
+ ### 2.2 UI & Styling
67
+
68
+ | Layer | Recommendation | Notes |
69
+ |-------|---------------|-------|
70
+ | **UI Components** | Lovable.ai generated + Radix UI primitives | Lovable generates the initial templates and components. Hand-roll custom logic on top. |
71
+ | **Styling** | Tailwind CSS v4 | Utility-first. Compatible with Lovable output. |
72
+ | **Component Library** | Shadcn UI (or custom) | Copy-pasteable components, fully customizable. |
73
+ | **Icons** | Lucide React | Consistent, tree-shakeable. |
74
+ | **Animations** | Framer Motion / tw-animate-css | For complex animations vs simple transitions. |
75
+
76
+ ### 2.3 State & Data
77
+
78
+ | Layer | Recommendation | Notes |
79
+ |-------|---------------|-------|
80
+ | **Server State** | TanStack React Query (v5) | Caching, refetching, optimistic updates. |
81
+ | **Client State** | Zustand | Lightweight, no boilerplate. |
82
+ | **Forms** | React Hook Form + Zod | Validation schema-driven. |
83
+ | **Routing** | React Router v7 / Next.js App Router | Depends on framework choice. |
84
+
85
+ ### 2.4 Testing
86
+
87
+ | Layer | Tool | Notes |
88
+ |-------|------|-------|
89
+ | **Unit / Component** | Vitest (preferred) or Jest 29 | Vitest is faster, native ESM, Vite-compatible. |
90
+ | **Component Testing** | Testing Library + jsdom | Test behavior, not implementation. |
91
+ | **E2E** | Playwright | Cross-browser, reliable, parallel. |
92
+ | **Visual Regression** | Playwright / Chromatic | For UI component libraries. |
93
+ | **Coverage** | c8 / istanbul | Minimum 80% line/branch. |
94
+
95
+ ---
96
+
97
+ ## 3. Project Scaffolding & Directory Structure
98
+
99
+ ### 3.1 Standard Project Layout
100
+
101
+ ```
102
+ project-name/
103
+ src/
104
+ app/ # Pages / Routes
105
+ pages/ # Pages
106
+ routes/ # Route definitions
107
+ layouts/ # Layout components
108
+ components/ # All components
109
+ ui/ # Base UI primitives (Lovable/Shadcn)
110
+ forms/ # Form-specific components
111
+ layout/ # App layout components
112
+ shared/ # Shared business components
113
+ hooks/ # Custom React hooks
114
+ lib/ # Utilities, API clients, helpers
115
+ store/ # Zustand stores
116
+ types/ # TypeScript type definitions
117
+ services/ # API service calls (domain-modularized)
118
+ employee/ # Employee domain services
119
+ employee-bio-detail.service.ts
120
+ employee-bank-detail.service.ts
121
+ auth/ # Auth domain services
122
+ auth.service.ts
123
+ users/ # User domain services
124
+ users.service.ts
125
+ api-client.ts # Shared API client (ONLY shared file at this level)
126
+ styles/ # Global styles (Tailwind entry)
127
+ test/ # Test setup, mocks, fixtures
128
+ public/ # Static assets
129
+ scripts/ # Build & automation scripts
130
+ .github/workflows/ # CI/CD pipeline definitions
131
+ .husky/ # Git hooks
132
+ docs/ # Project documentation
133
+ -- root config files --
134
+ ```
135
+
136
+ ### 3.2 Monorepo Layout (For Multiple Projects)
137
+
138
+ ```
139
+ project-name/
140
+ apps/
141
+ admin/ # Admin dashboard
142
+ web/ # Public website
143
+ packages/
144
+ config/eslint/ # Shared ESLint config
145
+ config/prettier/ # Shared Prettier config
146
+ config/tsconfig/ # Shared TypeScript config
147
+ ui/ # Shared component library
148
+ utils/ # Shared utilities
149
+ types/ # Shared type definitions
150
+ scripts/
151
+ -- root config files --
152
+ ```
153
+
154
+ ### 3.3 Standards
155
+
156
+ - Every project must have a README.md at the root
157
+ - Environment files: .env.example (committed), .env (gitignored)
158
+ - Config files: Use flat config where possible
159
+ - Directory naming: kebab-case for all directories
160
+ - File naming: kebab-case for all files, except PascalCase for React components (e.g., `UserProfile.tsx`)
161
+
162
+ #### 3.3.1 Domain-Based Modularization (Strictly Enforced)
163
+
164
+ All domain-specific files across **every** directory (`services/`, `hooks/`, `store/`, `components/`, `lib/`) must be organized into feature subdirectories. Flat/orphaned files at the root of any modularized directory are **prohibited** unless they are truly shared across all domains.
165
+
166
+ Every domain subdirectory **must** expose its contents via an `index.ts` barrel file. Parent code imports from the barrel, never from individual files within a domain.
167
+
168
+ | Directory | Modularization Rule | Barrel Required |
169
+ |-----------|---------------------|:---:|
170
+ | `services/` | One subdirectory per domain (e.g., `services/employee/`, `services/auth/`). Only `api-client.ts` may live at the root. | Yes |
171
+ | `hooks/` | Domain-specific hooks in `hooks/<domain>/`. Shared utility hooks (`useDebounce`) may live at root. | Yes |
172
+ | `store/` | Domain-specific Zustand stores in `store/<domain>/`. Cross-cutting stores only at root. | Yes |
173
+ | `components/` | Domain-specific components in `components/<domain>/`. Shared UI primitives in `components/ui/`. | Yes |
174
+ | `lib/` | Domain-specific utilities in `lib/<domain>/`. Cross-domain utilities may live at root. | Yes |
175
+ | `types/` | Domain-specific types in `types/<domain>/`. Shared types at root (`types/user.ts`). | Yes |
176
+
177
+ **Barrel File (`index.ts`) Convention**:
178
+
179
+ Every domain subdirectory must contain an `index.ts` that re-exports all public members. Parent-level code imports exclusively from the barrel:
180
+
181
+ ```
182
+ services/employee/
183
+ index.ts # Barrel: re-exports everything
184
+ employee-bio-detail.service.ts # Implementation file
185
+ employee-bank-detail.service.ts # Implementation file
186
+ employee-emergency-contact.service.ts # Implementation file
187
+ ```
188
+
189
+ ```typescript
190
+ // services/employee/index.ts (BARREL FILE -- REQUIRED)
191
+ export { getEmployeeBio } from './employee-bio-detail.service';
192
+ export { getEmployeeBankDetails, updateEmployeeBankDetails } from './employee-bank-detail.service';
193
+ export { getEmergencyContacts } from './employee-emergency-contact.service';
194
+ ```
195
+
196
+ ```typescript
197
+ // pages/EmployeeDashboard.tsx (PARENT CONSUMER -- imports from barrel only)
198
+ import { getEmployeeBio, getEmployeeBankDetails } from '@/services/employee';
199
+ // NOT: import { getEmployeeBio } from '@/services/employee/employee-bio-detail.service'; // BLOCKED
200
+ ```
201
+
202
+ **Violation examples** (these will be blocked by ESLint and CI):
203
+
204
+ ```
205
+ # BAD -- Flat files at directory root (BLOCKED)
206
+ services/employee-bio-detail.service.ts
207
+ hooks/useEmployeeLeave.ts
208
+ store/employeeLeaveStore.ts
209
+
210
+ # BAD -- Missing barrel file (BLOCKED)
211
+ services/employee/employee-bio-detail.service.ts # No index.ts in employee/
212
+
213
+ # BAD -- Deep import bypassing barrel (BLOCKED)
214
+ import { getEmployeeBio } from '@/services/employee/employee-bio-detail.service';
215
+
216
+ # GOOD -- Domain-modularized with barrel (REQUIRED)
217
+ services/employee/index.ts # Barrel exports
218
+ services/employee/employee-bio-detail.service.ts
219
+ services/employee/employee-bank-detail.service.ts
220
+ import { getEmployeeBio } from '@/services/employee';
221
+ ```
222
+
223
+ This rule is enforced via `eslint-plugin-boundaries` combined with `eslint-plugin-import` (see Section 5.2.1 for configuration).
224
+
225
+ ---
226
+
227
+ ## 4. TypeScript Configuration Standards
228
+
229
+ ### 4.1 Required Compiler Options
230
+
231
+ These options ensure a modern, strict, and performant TypeScript environment:
232
+
233
+ - **target: ES2022**: Specifies the JavaScript language version for the emitted output.
234
+ - **module: ESNext**: Sets the module system for the program to the latest ECMAScript standard.
235
+ - **moduleResolution: bundler**: Optimized module resolution for modern bundlers like Vite.
236
+ - **strict: true**: Enables all strict type-checking options for maximum safety.
237
+ - **esModuleInterop: true**: Ensures compatibility when importing CommonJS modules.
238
+ - **skipLibCheck: true**: Skips type checking of declaration files to speed up builds.
239
+ - **forceConsistentCasingInFileNames: true**: Prevents issues between case-sensitive and case-insensitive filesystems.
240
+ - **resolveJsonModule: true**: Allows importing .json files as modules.
241
+ - **isolatedModules: true**: Ensures each file can be safely transpiled independently.
242
+ - **allowJs: false**: Disallows plain JavaScript to enforce TypeScript usage.
243
+ - **noEmit: true**: Relies on the bundler (Vite) for emission, using tsc only for type checking.
244
+ - **jsx: react-jsx**: Enables the modern React JSX transform.
245
+ - **baseUrl: .**: Sets the root for non-relative module resolution.
246
+ - **paths**: Configures path aliases (e.g., @/*) for cleaner imports.
247
+
248
+ ```jsonc
249
+ {
250
+ "compilerOptions": {
251
+ "target": "ES2022",
252
+ "module": "ESNext",
253
+ "moduleResolution": "bundler",
254
+ "strict": true,
255
+ "esModuleInterop": true,
256
+ "skipLibCheck": true,
257
+ "forceConsistentCasingInFileNames": true,
258
+ "resolveJsonModule": true,
259
+ "isolatedModules": true,
260
+ "allowJs": false,
261
+ "noEmit": true,
262
+ "jsx": "react-jsx",
263
+ "baseUrl": ".",
264
+ "paths": { "@/*": ["src/*"] }
265
+ }
266
+ }
267
+ ```
268
+
269
+ ### 4.2 Strict Mode Checklist
270
+
271
+ - strict: true (enables all strict flags)
272
+ - noUncheckedIndexedAccess: true (prevent undefined access)
273
+ - exactOptionalPropertyTypes: true
274
+ - noPropertyAccessFromIndexSignature: true
275
+
276
+ ---
277
+
278
+ ## 5. Code Quality & Linting Standards
279
+
280
+ ### 5.1 ESLint (v9 Flat Config) - Required Plugins
281
+
282
+ | Plugin | Purpose |
283
+ |--------|---------|
284
+ | @eslint/js | ESLint recommended rules |
285
+ | typescript-eslint | TypeScript-specific rules |
286
+ | eslint-plugin-react | React-specific rules |
287
+ | eslint-plugin-react-hooks | React Hooks rules |
288
+ | eslint-plugin-import | Import ordering |
289
+ | eslint-plugin-unicorn | Modern JS best practices |
290
+ | eslint-plugin-prettier | Prettier as ESLint rule |
291
+ | eslint-plugin-jsx-a11y | Accessibility checks |
292
+ | eslint-plugin-boundaries | Enforce domain-based modularization and folder dependency rules |
293
+
294
+ ### 5.2 Naming Conventions -- Complete Reference
295
+
296
+ #### Core Principle: Names Must Be Self-Documenting
297
+
298
+ A variable name should tell you what it contains, not how it's implemented. If you need a comment to explain what a variable holds, the name is wrong.
299
+
300
+ **Single-letter variables are strictly prohibited** except:
301
+ - Loop index i, j, k inside a for loop
302
+ - Array callback _ for unused parameters
303
+ - Math coordinates x, y, z (must be documented)
304
+
305
+ #### Naming Reference Table
306
+
307
+ | Entity | Convention | Good Example | Bad Example |
308
+ |--------|-----------|-------------|-------------|
309
+ | Files (General) | kebab-case | user-service.ts | UserService.ts |
310
+ | Files (React Components) | PascalCase | UserProfile.tsx | user-profile.tsx |
311
+ | Directories | kebab-case | user-profile/ | UserProfile/ |
312
+ | Variables (general) | camelCase | userCount, isLoading | uc, l, data |
313
+ | Booleans | camelCase + prefix | isLoading, hasError, canEdit | loading, error |
314
+ | Functions | camelCase (verb+noun) | getUserById(), handleSubmit() | get(), doStuff() |
315
+ | Constants (module) | UPPER_SNAKE_CASE | MAX_RETRY_COUNT | maxRetryCount |
316
+ | Types/Interfaces | PascalCase | UserProfileProps | userProfile |
317
+ | Enums | PascalCase + UPPER values | Role.ADMIN | role.admin |
318
+ | Event handlers | handle + Event | handleSubmit | submitHandler |
319
+ | Handler props | on + Event | onSubmit, onClick | submit, clicked |
320
+ | Env variables | UPPER_SNAKE + prefix | VITE_API_URL | apiUrl |
321
+ | Mock data | mock + Entity | mockUser, mockResponse | testData |
322
+
323
+ #### Banned Variable Names
324
+
325
+ | Banned | Why | Use Instead |
326
+ |--------|-----|-------------|
327
+ | const data | Means anything | const users, const response |
328
+ | const temp | Temporary to whom? | const formattedDate |
329
+ | const arr | Hungarian + meaningless | const permissions |
330
+ | const obj | Everything is an object | const config |
331
+ | const str | Hungarian notation | const message |
332
+ | const result | All functions return a result | const validationResult |
333
+ | const myVar | my prefix adds nothing | const username |
334
+ | function handle() | Handle what? | function handleFormSubmit() |
335
+
336
+ #### Boolean Naming
337
+
338
+ Booleans must use a prefix that makes true/false obvious:
339
+ ```
340
+ GOOD: isLoading, hasPermission, canSubmit, shouldRender, isVisible, wasProcessed
341
+ BAD: loading, permission, submit, render, visible, processed
342
+ ```
343
+
344
+ #### ESLint Enforcement Rules
345
+
346
+ ```javascript
347
+ rules: {
348
+ // Ban single-letter variables (minimum 2 characters)
349
+ 'id-length': ['error', {
350
+ min: 2, max: 50,
351
+ exceptions: ['i', 'j', 'k', '_', 'x', 'y', 'z'],
352
+ properties: 'never',
353
+ }],
354
+ // Enforce camelCase
355
+ camelcase: ['error', { properties: 'never', ignoreDestructuring: false, allow: ['^UNSAFE_'] }],
356
+ // Enforce kebab-case file names, except for PascalCase React components
357
+ 'unicorn/filename-case': ['error', {
358
+ case: 'kebabCase',
359
+ ignore: ['^[A-Z].*\\.tsx$'],
360
+ }],
361
+ // Enforce PascalCase for React components
362
+ 'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
363
+ }
364
+ ```
365
+
366
+ #### 5.2.1 Domain-Based Modularization Enforcement (eslint-plugin-boundaries + eslint-plugin-import)
367
+
368
+ Install the required plugins:
369
+
370
+ ```bash
371
+ pnpm add -D eslint-plugin-boundaries eslint-plugin-import
372
+ ```
373
+
374
+ Configuration for enforcing domain-based folder modularization across **all** directories
375
+ (`services/`, `hooks/`, `store/`, `components/`, `lib/`, `types/`), including barrel file (`index.ts`) enforcement:
376
+
377
+ ```javascript
378
+ // eslint.config.js (flat config)
379
+ import boundaries from "eslint-plugin-boundaries";
380
+ import importPlugin from "eslint-plugin-import";
381
+
382
+ export default [
383
+ // ... other configs
384
+ {
385
+ plugins: { boundaries, import: importPlugin },
386
+ settings: {
387
+ "boundaries/include": ["src/**/*"],
388
+ "boundaries/elements": [
389
+ // Shared/global files (can be imported by any domain)
390
+ {
391
+ mode: "full",
392
+ type: "shared",
393
+ pattern: [
394
+ "src/services/api-client.ts",
395
+ "src/hooks/use-debounce.ts",
396
+ "src/hooks/use-media-query.ts",
397
+ "src/components/ui/**",
398
+ "src/lib/*.ts",
399
+ "src/lib/*.tsx",
400
+ "src/types/!(*/**).ts",
401
+ ],
402
+ },
403
+ // Employee domain -- all directories
404
+ {
405
+ mode: "full",
406
+ type: "employee",
407
+ pattern: [
408
+ "src/services/employee/**",
409
+ "src/hooks/employee/**",
410
+ "src/store/employee/**",
411
+ "src/components/employee/**",
412
+ "src/lib/employee/**",
413
+ "src/types/employee/**",
414
+ ],
415
+ },
416
+ // Auth domain -- all directories
417
+ {
418
+ mode: "full",
419
+ type: "auth",
420
+ pattern: [
421
+ "src/services/auth/**",
422
+ "src/hooks/auth/**",
423
+ "src/store/auth/**",
424
+ "src/components/auth/**",
425
+ "src/lib/auth/**",
426
+ "src/types/auth/**",
427
+ ],
428
+ },
429
+ // Users domain -- all directories
430
+ {
431
+ mode: "full",
432
+ type: "users",
433
+ pattern: [
434
+ "src/services/users/**",
435
+ "src/hooks/users/**",
436
+ "src/store/users/**",
437
+ "src/components/users/**",
438
+ "src/lib/users/**",
439
+ "src/types/users/**",
440
+ ],
441
+ },
442
+ ],
443
+ },
444
+ rules: {
445
+ // === BOUNDARIES RULES ===
446
+
447
+ // BLOCK: flat/orphaned domain files at directory roots (enforces subdirectory pattern)
448
+ "boundaries/entry-point": [
449
+ "error",
450
+ {
451
+ default: "disallow",
452
+ rules: [
453
+ // Allow ONLY api-client.ts at the services/ root level
454
+ {
455
+ target: ["src/services/**/*.ts"],
456
+ allow: "src/services/api-client.ts",
457
+ },
458
+ // Allow shared utility hooks at root but NOT domain-specific ones
459
+ {
460
+ target: ["src/hooks/use-employee*.ts", "src/hooks/use-auth*.ts", "src/hooks/use-user*.ts"],
461
+ disallow: "src/hooks/",
462
+ },
463
+ // No domain-specific files at store/ root
464
+ {
465
+ target: ["src/store/**/*.ts"],
466
+ allow: "src/store/index.ts",
467
+ },
468
+ ],
469
+ },
470
+ ],
471
+
472
+ // BLOCK: cross-domain imports (e.g., employee component cannot import from auth services)
473
+ "boundaries/element-types": [
474
+ "error",
475
+ {
476
+ default: "disallow",
477
+ rules: [
478
+ // Shared files can be imported by any domain
479
+ { from: ["employee", "auth", "users"], allow: ["shared"] },
480
+ // Domains cannot import from other domains
481
+ { from: ["employee"], disallow: ["auth", "users"] },
482
+ { from: ["auth"], disallow: ["employee", "users"] },
483
+ { from: ["users"], disallow: ["employee", "auth"] },
484
+ // Shared can import from shared
485
+ { from: ["shared"], allow: ["shared"] },
486
+ ],
487
+ },
488
+ ],
489
+
490
+ // === IMPORT RULES: BLOCK deep imports bypassing barrel files ===
491
+
492
+ // BLOCK: importing from a non-index file inside a domain subdirectory
493
+ "import/no-internal-modules": [
494
+ "error",
495
+ {
496
+ allow: [
497
+ // Only allow importing the barrel (index.ts) from each domain directory
498
+ "src/services/*/index",
499
+ "src/hooks/*/index",
500
+ "src/store/*/index",
501
+ "src/components/*/index",
502
+ "src/lib/*/index",
503
+ "src/types/*/index",
504
+ // Allow shared/utility files
505
+ "src/services/api-client",
506
+ "src/hooks/use-debounce",
507
+ "src/hooks/use-media-query",
508
+ "src/components/ui/**",
509
+ "src/lib/*",
510
+ "src/types/*",
511
+ // Allow root-level globals
512
+ "src/app/**",
513
+ "src/styles/**",
514
+ "src/test/**",
515
+ ],
516
+ },
517
+ ],
518
+
519
+ // BLOCK: importing from index files across domain boundaries (backup for boundaries plugin)
520
+ "import/no-restricted-paths": [
521
+ "error",
522
+ {
523
+ zones: [
524
+ // Employee domain may not import from other domains
525
+ {
526
+ target: "./src/**/employee/**",
527
+ from: "./src/**/auth/**",
528
+ message: "Employee domain must not import from Auth domain. Use shared interfaces instead.",
529
+ },
530
+ {
531
+ target: "./src/**/employee/**",
532
+ from: "./src/**/users/**",
533
+ message: "Employee domain must not import from Users domain. Use shared interfaces instead.",
534
+ },
535
+ // (repeat for each domain pair)
536
+ ],
537
+ },
538
+ ],
539
+ },
540
+ },
541
+ ];
542
+ ```
543
+
544
+ **Enforcement Summary**:
545
+
546
+ | Rule | What It Catches | Severity |
547
+ |------|----------------|----------|
548
+ | `boundaries/entry-point` | Flat domain files at directory roots (e.g., `services/employeeService.ts` instead of `services/employee/`) | `error` |
549
+ | `boundaries/element-types` | Cross-domain imports (e.g., `employee` component importing from `auth` services) | `error` |
550
+ | `import/no-internal-modules` | **Deep imports bypassing barrel files** (e.g., `from '@/services/employee/employee-bio-detail.service'` instead of `from '@/services/employee'`) | `error` |
551
+ | `import/no-restricted-paths` | Backup enforcement of cross-domain import restrictions | `error` |
552
+
553
+ **Barrel File Enforcement Flow**:
554
+
555
+ ```
556
+ ✅ ALLOWED: import { getBio } from '@/services/employee';
557
+ // Resolves to services/employee/index.ts (barrel)
558
+
559
+ ❌ BLOCKED: import { getBio } from '@/services/employee/employee-bio-detail.service';
560
+ // Deep import bypassing barrel -- `import/no-internal-modules` catches this
561
+
562
+ ✅ ALLOWED: import { useEmployeeBio } from '@/hooks/employee';
563
+ // Resolves to hooks/employee/index.ts (barrel)
564
+
565
+ ❌ BLOCKED: import { useEmployeeBio } from '@/hooks/employee/useEmployeeBio';
566
+ // Deep import bypassing barrel
567
+ ```
568
+
569
+ **Adding a New Domain**:
570
+ 1. Create the domain subdirectories across all folders:
571
+ ```
572
+ services/<domain>/
573
+ hooks/<domain>/
574
+ store/<domain>/
575
+ components/<domain>/
576
+ lib/<domain>/
577
+ types/<domain>/
578
+ ```
579
+ 2. Create an `index.ts` barrel file in each subdirectory
580
+ 3. Add the domain element type to `boundaries/elements`
581
+ 4. Add the `from`/`disallow` rules to `boundaries/element-types`
582
+ 5. Add the domain barrel paths to `import/no-internal-modules` allow list
583
+ 6. Add cross-domain restrictions to `import/no-restricted-paths`
584
+ 7. The domain starts isolated; update rules if cross-domain communication is needed (via shared interfaces only)
585
+
586
+ #### Naming Decision Tree
587
+
588
+ ```
589
+ Is it a boolean? --> is/has/can/should/did/was prefix
590
+ Is it a count? --> count/total/num/index suffix
591
+ Is it a list? --> Plural noun or List suffix
592
+ Is it an entity? --> Specific singular noun
593
+ Is it a getter? --> get/fetch prefix
594
+ Is it a transformer?--> format/convert prefix
595
+ ```
596
+
597
+ #### Good vs Bad Examples
598
+
599
+ ```typescript
600
+ // BAD -- Would fail code review
601
+ const d = new Date();
602
+ const r = await fetch('/api/users');
603
+ const arr = [1, 2, 3];
604
+ const temp = users.filter(u => u.active);
605
+ function get() { }
606
+ function handle() { }
607
+
608
+ // GOOD -- Self-documenting
609
+ const currentDate = new Date();
610
+ const response = await fetch('/api/users');
611
+ const userIds = [1, 2, 3];
612
+ const activeUsers = users.filter(user => user.isActive);
613
+ function getActiveUsers() { }
614
+ function handleFormSubmit() { }
615
+ ```
616
+
617
+ ### 5.3 Import Order Standard
618
+
619
+ ```typescript
620
+ // 1. Built-in Node modules
621
+ import fs from 'fs';
622
+ // 2. External dependencies
623
+ import { useState } from 'react';
624
+ // 3. Internal modules (using @/ alias)
625
+ import { Button } from '@/components/ui/button';
626
+ // 4. Parent imports
627
+ import { formatDate } from '../utils/date';
628
+ // 5. Sibling imports
629
+ import { UserCard } from './user-card';
630
+ ```
631
+
632
+ ### 5.4 Lovable-Generated Code Handling
633
+
634
+ When Lovable generates UI templates, always:
635
+ 1. Run generated code through ESLint + Prettier before committing.
636
+ 2. Extract reusable components into components/shared/ directory.
637
+ 3. Add proper TypeScript types to any untyped props.
638
+ 4. Ensure generated components pass accessibility checks.
639
+ 5. Add JSDoc comments to any complex generated logic.
640
+
641
+ ---
642
+
643
+ ## 6. Formatting Standards
644
+
645
+ ### 6.1 Prettier Configuration
646
+
647
+ ```javascript
648
+ // .prettierrc.js
649
+ export default {
650
+ semi: true,
651
+ singleQuote: true,
652
+ tabWidth: 2,
653
+ useTabs: false,
654
+ trailingComma: 'es5',
655
+ printWidth: 100,
656
+ bracketSpacing: true,
657
+ arrowParens: 'always',
658
+ endOfLine: 'lf',
659
+ plugins: ['prettier-plugin-organize-imports'],
660
+ };
661
+ ```
662
+
663
+ ### 6.2 Format-On-Save (VS Code)
664
+
665
+ Every project must have .vscode/settings.json:
666
+ ```json
667
+ {
668
+ "editor.formatOnSave": true,
669
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
670
+ "editor.codeActionsOnSave": {
671
+ "source.fixAll.eslint": "explicit"
672
+ }
673
+ }
674
+ ```
675
+
676
+ ---
677
+
678
+ ## 7. UI Component Standards (Lovable Integration)
679
+
680
+ ### 7.1 Lovable Workflow
681
+
682
+ 1. **Design in Lovable**: Product designer creates UI mockups/templates.
683
+ 2. **Export clean components**: Export generated React components with Tailwind styling.
684
+ 3. **Integrate into project**: Place components in src/components/ui/ or shared/.
685
+ 4. **Add business logic**: Connect components to stores, API calls, and routing.
686
+ 5. **Test**: Write unit tests for logic, visual regression for UI.
687
+ 6. **Review**: Code review must verify generated code meets project standards.
688
+
689
+ ### 7.2 Component Architecture
690
+
691
+ ```
692
+ src/components/
693
+ ui/ # Base UI primitives (from Lovable/Shadcn)
694
+ Button.tsx
695
+ Input.tsx
696
+ Dialog.tsx
697
+ Table.tsx
698
+ forms/ # Form-specific compositions
699
+ LoginForm.tsx
700
+ SignupForm.tsx
701
+ layout/ # Layout components
702
+ Header.tsx
703
+ Sidebar.tsx
704
+ shared/ # Domain-specific shared components
705
+ UserAvatar.tsx
706
+ DataTable.tsx
707
+ ```
708
+
709
+ ### 7.3 Component Best Practices
710
+
711
+ - One component per file (except small utility components)
712
+ - Props interface defined and exported
713
+ - Default export for pages, named export for reusable components
714
+ - Composition over configuration: prefer children and slots over boolean props
715
+ - Lovable components should be presentational -- business logic in hooks and stores
716
+
717
+ ### 7.3.1 Error Boundaries (Required)
718
+
719
+ Every route-level component must be wrapped in an Error Boundary. Implement a standard `<ErrorBoundary>` component:
720
+
721
+ ```tsx
722
+ import { Component, type ReactNode } from 'react';
723
+ import { logger } from '@/lib/logger';
724
+
725
+ interface ErrorBoundaryProps {
726
+ fallback?: ReactNode;
727
+ children: ReactNode;
728
+ onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
729
+ }
730
+
731
+ interface ErrorBoundaryState {
732
+ hasError: boolean;
733
+ error: Error | null;
734
+ }
735
+
736
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
737
+ state: ErrorBoundaryState = { hasError: false, error: null };
738
+
739
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
740
+ return { hasError: true, error };
741
+ }
742
+
743
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
744
+ logger.error('ErrorBoundary caught an error', { error, componentStack: errorInfo.componentStack });
745
+ this.props.onError?.(error, errorInfo);
746
+ }
747
+
748
+ handleReset = () => this.setState({ hasError: false, error: null });
749
+
750
+ render() {
751
+ if (this.state.hasError) {
752
+ if (this.props.fallback) return this.props.fallback;
753
+ return (
754
+ <div role="alert" className="flex flex-col items-center justify-center min-h-[400px] p-8">
755
+ <h2 className="text-xl font-semibold mb-2">Something went wrong</h2>
756
+ <p className="text-muted-foreground mb-4">An unexpected error occurred.</p>
757
+ <button onClick={this.handleReset} className="px-4 py-2 bg-primary text-primary-foreground rounded-md">
758
+ Try Again
759
+ </button>
760
+ </div>
761
+ );
762
+ }
763
+ return this.props.children;
764
+ }
765
+ }
766
+ ```
767
+
768
+ **Usage**:
769
+ ```tsx
770
+ // Route-level wrapping
771
+ <ErrorBoundary onError={(err) => captureException(err)}>
772
+ <EmployeeDashboard />
773
+ </ErrorBoundary>
774
+ ```
775
+
776
+ **Rules**:
777
+ - One ErrorBoundary per route (not per component) for granularity
778
+ - Log errors to Sentry via `onError` callback
779
+ - Never wrap individual small components (<50 lines) in their own ErrorBoundary
780
+ - Fallback UI must include a "Try Again" reset action
781
+
782
+ ### 7.3.2 Code Splitting & Lazy Loading (Required for Routes)
783
+
784
+ All page-level components must be lazy-loaded to reduce initial bundle size.
785
+
786
+ ```tsx
787
+ import { lazy, Suspense } from 'react';
788
+ import { ErrorBoundary } from '@/components/shared/error-boundary';
789
+ import { PageSkeleton } from '@/components/ui/page-skeleton';
790
+
791
+ const EmployeeDashboard = lazy(() => import('@/pages/employee/employee-dashboard'));
792
+ const EmployeeProfile = lazy(() => import('@/pages/employee/employee-profile'));
793
+
794
+ export function EmployeeRoutes() {
795
+ return (
796
+ <ErrorBoundary>
797
+ <Suspense fallback={<PageSkeleton />}>
798
+ <Routes>
799
+ <Route path="dashboard" element={<EmployeeDashboard />} />
800
+ <Route path="profile/:id" element={<EmployeeProfile />} />
801
+ </Routes>
802
+ </Suspense>
803
+ </ErrorBoundary>
804
+ );
805
+ }
806
+ ```
807
+
808
+ **Rules**:
809
+ - Every page/route component must use `React.lazy()` + dynamic `import()`
810
+ - Every lazy route must have a `<Suspense>` wrapper with a skeleton fallback
811
+ - Prefetch critical routes (dashboard, home) on link hover or visibility
812
+ - Named exports are required for lazy-loaded modules: `export function EmployeeDashboard()`
813
+ - Chunk naming: use `webpackChunkName: 'employee'` comment for Vite/webpack
814
+
815
+ ### 7.4.1 Loading & Error State Patterns
816
+
817
+ **Loading States**:
818
+
819
+ | Context | Pattern | When |
820
+ |---------|---------|------|
821
+ | Page load | Skeleton screen | Initial page navigation |
822
+ | Table/list data | Skeleton rows or spinner overlay | Data fetching |
823
+ | Button action | Spinner inside button + disabled state | Form submit, save, delete |
824
+ | File upload | Progress bar | Large uploads |
825
+
826
+ Debounce loader appearance: do not show any loading indicator for fetches that resolve in < 300ms (avoid flash).
827
+
828
+ ```tsx
829
+ // Standard page pattern
830
+ function EmployeePage() {
831
+ const { data, isLoading, isError, error, refetch } = useEmployee(id);
832
+
833
+ if (isLoading) return <PageSkeleton />;
834
+ if (isError) return <ErrorState error={error} onRetry={refetch} />;
835
+ if (!data) return <EmptyState message="No employee data found" action={<CreateEmployeeButton />} />;
836
+
837
+ return <EmployeeProfile employee={data} />;
838
+ }
839
+ ```
840
+
841
+ **Error States** — use a standard `<ErrorState>` component:
842
+
843
+ ```tsx
844
+ interface ErrorStateProps {
845
+ error?: Error | string;
846
+ onRetry?: () => void;
847
+ title?: string;
848
+ }
849
+
850
+ export function ErrorState({ error, onRetry, title = 'Something went wrong' }: ErrorStateProps) {
851
+ return (
852
+ <div role="alert" className="flex flex-col items-center justify-center min-h-[300px] p-6 text-center">
853
+ <AlertTriangle className="w-12 h-12 text-destructive mb-4" />
854
+ <h3 className="text-lg font-semibold mb-1">{title}</h3>
855
+ <p className="text-muted-foreground mb-4 max-w-md">
856
+ {typeof error === 'string' ? error : error?.message || 'An unexpected error occurred. Please try again.'}
857
+ </p>
858
+ {onRetry && (
859
+ <button onClick={onRetry} className="px-4 py-2 bg-primary text-primary-foreground rounded-md">
860
+ Try Again
861
+ </button>
862
+ )}
863
+ </div>
864
+ );
865
+ }
866
+ ```
867
+
868
+ ### 7.4.2 Accessibility Standards (WCAG 2.1 AA — Mandatory)
869
+
870
+ Every UI component must meet WCAG 2.1 AA standards. The following are **non-negotiable**:
871
+
872
+ | Requirement | Implementation |
873
+ |-------------|---------------|
874
+ | Color contrast | Minimum 4.5:1 for text, 3:1 for large text. Verify with axe DevTools. |
875
+ | Keyboard navigation | All interactive elements reachable via Tab. Logical tab order. Visible focus ring. |
876
+ | Skip link | Skip-to-content link as first focusable element on every page. |
877
+ | Screen reader | `aria-label` on icon-only buttons. `aria-describedby` linking inputs to error messages. |
878
+ | Form accessibility | Every `<input>` must have an associated `<label>`. Error messages linked via `aria-describedby`. |
879
+ | Modals/Dialogs | Focus trap inside modal. Escape to close. `aria-modal="true"`. |
880
+ | Images | All `<img>` must have `alt` attribute (descriptive or empty for decorative). |
881
+ | Landmarks | Use semantic HTML: `<header>`, `<main>`, `<nav>`, `<footer>`. |
882
+
883
+ **Required tools**: axe DevTools browser extension, eslint-plugin-jsx-a11y (already enforced via ESLint).
884
+
885
+ ---
886
+
887
+ ### 7.5 Table, Navigation, and Branding Standards
888
+
889
+ #### Filterable and Sortable Tables (Required)
890
+
891
+ Every data table MUST support sortable column headers, global and per-column filtering, debounced search, persistent sort preference, visible active filters, and a no-results state.
892
+
893
+ Required table features:
894
+ - Sort on click for all non-action columns with sort direction indicator
895
+ - Filter input above table with 300-400ms debounce
896
+ - Column visibility toggle
897
+ - Row selection with selected count and bulk actions
898
+ - Server-side pagination with page numbers, prev/next, and page size selector
899
+ - Empty state with friendly text and a primary action
900
+ - Loading state using table skeleton or row spinners
901
+ - Error state with retry button
902
+
903
+ #### Jump-to-Top Button (Required on Long Pages)
904
+
905
+ Pages taller than 2x viewport MUST include a fixed back-to-top button:
906
+ - Appears after scrolling 500px
907
+ - Smooth scroll to top
908
+ - Hidden when at top
909
+ - Accessible: keyboard navigable with aria-label
910
+ - Use an up chevron or arrow icon
911
+
912
+ #### Go Back / Go to Previous (Context-Aware Navigation)
913
+
914
+ Every page except the home/landing page MUST provide navigation aids:
915
+ - Breadcrumbs showing full path: Home > Reports > Financial > Q1 2026
916
+ - Back button returning to parent list or previous page
917
+ - Context-aware return preserving list filters and pagination via query params
918
+
919
+ #### Corporate Branding (Required)
920
+
921
+ Every application MUST implement consistent corporate branding:
922
+ - Primary logo in header/sidebar, clickable to home, with favicon and Apple touch icon
923
+ - Brand colors defined in Tailwind config with minimum 4.5:1 contrast ratio
924
+ - Typography: Inter or system fonts, defined heading scale, 1rem base body text
925
+ - Consistent spacing: 4px base unit, standard page padding, consistent gaps
926
+ - Approved loading and empty states using corporate colors
927
+ - 404 and error pages with corporate logo, friendly message, and primary action
928
+
929
+ ---
930
+
931
+
932
+ ---
933
+
934
+ ## 8. Testing Standards
935
+
936
+ ### 8.1 Testing Pyramid
937
+
938
+ ```
939
+ /\
940
+ /E2E\ Playwright (critical user flows)
941
+ /------\
942
+ /Integr.\ API + Component integration tests
943
+ /----------\
944
+ / Unit Tests \ Vitest + Testing Library (fast, isolated)
945
+ /--------------\
946
+ ```
947
+
948
+ ### 8.2 Coverage Requirements
949
+
950
+ | Level | Minimum Coverage | Notes |
951
+ |-------|-----------------|-------|
952
+ | Line | 80% | Enforced in CI pipeline |
953
+ | Branch | 75% | Enforced in CI pipeline |
954
+ | New Code | 90% | PR checks must pass |
955
+ | Critical Paths | 100% | Auth, payments, data export |
956
+
957
+ ### 8.3 Test Structure
958
+
959
+ ```typescript
960
+ describe('UserProfile', () => {
961
+ describe('rendering', () => {
962
+ it('should display user name when data is loaded', () => {
963
+ const user = { name: 'John Doe', email: 'john@example.com' };
964
+ render(<UserProfile user={user} />);
965
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
966
+ });
967
+
968
+ it('should show loading state when data is fetching', () => {
969
+ render(<UserProfile isLoading={true} />);
970
+ expect(screen.getByRole('status')).toBeInTheDocument();
971
+ });
972
+ });
973
+
974
+ describe('error handling', () => {
975
+ it('should display error message when API fails', () => {
976
+ render(<UserProfile error="Failed to load" />);
977
+ expect(screen.getByText(/failed/i)).toBeInTheDocument();
978
+ });
979
+ });
980
+ });
981
+ ```
982
+
983
+ ### 8.4 E2E Standards (Playwright)
984
+
985
+ - One spec file per critical user journey
986
+ - Use data-testid attributes (not CSS classes) for selectors
987
+ - Run against production-like environment (staging)
988
+ - Include: login flow, CRUD operations, error states
989
+
990
+ ---
991
+
992
+ ## 9. Git Workflow & Commit Standards
993
+
994
+ ### 9.1 Branch Strategy
995
+
996
+ ```
997
+ main # Production-ready code
998
+ develop # Integration branch
999
+ feature/* # New features (branch from develop)
1000
+ bugfix/* # Bug fixes (branch from develop)
1001
+ hotfix/* # Critical fixes (branch from main, merge back)
1002
+ ```
1003
+
1004
+ ### 9.2 Commit Message Format
1005
+
1006
+ ```
1007
+ <type>(<scope>): <description>
1008
+ [optional body]
1009
+ ```
1010
+
1011
+ **Types**: feat, fix, security, perf, docs, style, refactor, test, chore
1012
+
1013
+ **Examples**:
1014
+ ```
1015
+ feat(auth): add JWT refresh token flow
1016
+ fix(profile): resolve avatar upload timeout
1017
+ security(api): implement rate limiting on login
1018
+ chore(deps): upgrade React to v19
1019
+ ```
1020
+
1021
+ ### 9.3 Git Hooks (Husky)
1022
+
1023
+ | Hook | Action | Tool |
1024
+ |------|--------|------|
1025
+ | pre-commit | Lint staged files | lint-staged |
1026
+ | commit-msg | Validate commit message | commitlint |
1027
+ | pre-push | Run lint + modularization + type check + tests | eslint + check-modularization + tsc + vitest |
1028
+
1029
+ **lint-staged config**:
1030
+ ```json
1031
+ {
1032
+ "src/**/*.{ts,tsx}": ["eslint --fix", "prettier --write"],
1033
+ "*.{json,md,yaml,yml}": ["prettier --write"]
1034
+ }
1035
+ ```
1036
+
1037
+ ---
1038
+
1039
+ ## 10. Code Review Standards
1040
+
1041
+ ### 10.1 PR Pre-Submission Checklist
1042
+
1043
+ > **PR Template Required**: Every repository must contain `.github/PULL_REQUEST_TEMPLATE.md`. The canonical templates are provided by `@aoholdings/frontend-standards` (frontend) and `@aoholdings/backend-standards` (backend). They are auto-copied into `.github/` on `pnpm install`. All PRs must follow the template structure. CI validates this via the PR checklist gates.
1044
+
1045
+ - [ ] Code compiles without errors (tsc --noEmit)
1046
+ - [ ] All lint rules pass
1047
+ - [ ] All tests pass
1048
+ - [ ] New code has >=90% test coverage
1049
+ - [ ] No .only in test files
1050
+ - [ ] No console.log or debugger statements
1051
+ - [ ] No hardcoded secrets, API keys, or URLs
1052
+ - [ ] Branch is up to date with develop/main
1053
+ - [ ] PR description follows the template (`.github/PULL_REQUEST_TEMPLATE.md`)
1054
+
1055
+ ### 10.2 PR Description Template
1056
+
1057
+ > **Enforcement**: Every repository must contain `.github/PULL_REQUEST_TEMPLATE.md`. The canonical PR template is provided by `@aoholdings/frontend-standards` and auto-copied on `pnpm install`. CI will flag PRs that do not follow the template structure. See [Section 19.10](#1910-enforcement-checklist-for-new-projects) for the enforcement checklist.
1058
+
1059
+ ```markdown
1060
+ ## Description
1061
+ [Brief description of the change]
1062
+
1063
+ ## Type of Change
1064
+ - [ ] feat: New feature
1065
+ - [ ] fix: Bug fix
1066
+ - [ ] refactor: Code restructure
1067
+ - [ ] test: Test changes
1068
+ - [ ] docs: Documentation
1069
+ - [ ] chore: Build/tooling
1070
+
1071
+ ## Testing Done
1072
+ - [ ] Unit tests added/updated
1073
+ - [ ] Manual testing performed
1074
+ - [ ] E2E tests (if applicable)
1075
+
1076
+ ## Screenshots (if UI change)
1077
+
1078
+ ## Checklist
1079
+ - [ ] Code follows project style guidelines
1080
+ - [ ] Self-review completed
1081
+ - [ ] No breaking changes (or documented)
1082
+ - [ ] Security implications considered
1083
+ - [ ] Accessibility checked (if UI change)
1084
+ ```
1085
+
1086
+ ### 10.3 Review Checklist
1087
+
1088
+ **Functional**:
1089
+ - Does the code do what its supposed to?
1090
+ - Are edge cases handled (loading, empty, error states)?
1091
+ - Are there appropriate tests?
1092
+
1093
+ **Architecture**:
1094
+ - Is the component properly decomposed?
1095
+ - Is business logic separated from UI?
1096
+ - Are state management decisions appropriate?
1097
+
1098
+ **Security**:
1099
+ - Are user inputs validated and sanitized?
1100
+ - Are secrets exposed anywhere?
1101
+ - Is authentication/authorization enforced?
1102
+
1103
+ **Performance**:
1104
+ - Are there unnecessary re-renders?
1105
+ - Are large lists properly virtualized?
1106
+ - Are images optimized?
1107
+
1108
+ **Accessibility**:
1109
+ - Are all interactive elements keyboard-accessible?
1110
+ - Do images have alt text?
1111
+ - Is color contrast sufficient?
1112
+
1113
+ **Lovable Code**:
1114
+ - Has the generated code been cleaned up (no unused imports, proper types)?
1115
+ - Are generated components properly integrated?
1116
+
1117
+ ### 10.4 Review Process Flow
1118
+
1119
+ ```
1120
+ PR Created
1121
+ |
1122
+ Auto Checks (CI): Lint, Types, Tests, Coverage
1123
+ |
1124
+ |-- FAIL --> Author fixes --> PR updated
1125
+ |
1126
+ v PASS
1127
+ Peer Review (at least 1 senior dev)
1128
+ |
1129
+ |-- CHANGES REQUESTED --> Author addresses --> Re-review
1130
+ |
1131
+ v APPROVED
1132
+ Second Approval (for main branch)
1133
+ |
1134
+ v
1135
+ Merge (Squash commit to maintain clean history)
1136
+ |
1137
+ v
1138
+ Auto-deploy to staging (if main/develop)
1139
+ ```
1140
+
1141
+ ---
1142
+
1143
+ ## 11. CI/CD Pipeline Standards
1144
+
1145
+ ### 11.1 Pipeline Stages
1146
+
1147
+ ```
1148
+ CI PIPELINE (Runs on every PR and push to develop/main):
1149
+ 1. Checkout + Setup Node.js + pnpm
1150
+ 2. Cache dependencies
1151
+ 3. pnpm install --frozen-lockfile
1152
+ 4. pnpm lint (must pass)
1153
+ 5. pnpm check-types (must pass)
1154
+ 6. pnpm test:ci (>=80% coverage)
1155
+ 7. Security audit (pnpm audit / npm audit)
1156
+ 8. Build application
1157
+ 9. Upload test results + coverage
1158
+
1159
+ CD PIPELINE (Push to develop):
1160
+ 1. Build Docker image
1161
+ 2. Push to container registry
1162
+ 3. Deploy to Dev (auto)
1163
+ 4. Run smoke tests
1164
+
1165
+ CD PIPELINE (Push to main + manual approval):
1166
+ 1. Build Docker image
1167
+ 2. Push to container registry
1168
+ 3. Deploy to Staging (auto)
1169
+ 4. E2E tests
1170
+ 5. Manual approval gate
1171
+ 6. Deploy to Production (rolling update)
1172
+ 7. Health check + smoke tests
1173
+ ```
1174
+
1175
+ ### 11.2 CI Configuration (GitHub Actions Example)
1176
+
1177
+ ```yaml
1178
+ name: CI Pipeline
1179
+ on:
1180
+ push: { branches: [main, develop] }
1181
+ pull_request: { branches: [main, develop] }
1182
+
1183
+ jobs:
1184
+ quality:
1185
+ runs-on: ubuntu-latest
1186
+ steps:
1187
+ - uses: actions/checkout@v4
1188
+ - uses: actions/setup-node@v4
1189
+ with: { node-version: "20" }
1190
+ - uses: pnpm/action-setup@v2
1191
+ with: { version: 9 }
1192
+
1193
+ - run: pnpm install --frozen-lockfile
1194
+ - run: pnpm lint
1195
+ - run: pnpm check-types
1196
+ - run: pnpm test:ci
1197
+ - run: pnpm audit --audit-level=high
1198
+ - run: pnpm build
1199
+
1200
+ - uses: codecov/codecov-action@v4
1201
+ with:
1202
+ files: ./coverage/cobertura-coverage.xml
1203
+ ```
1204
+
1205
+ ### 11.3 Non-Negotiables
1206
+
1207
+ 1. Lockfile must be committed: Reproducible builds depend on it. **pnpm-lock.yaml** is the standard for CI/CD.
1208
+ 2. Pipeline must fail on lint or test failures. No continueOnError.
1209
+ 3. Security scanning required: npm audit or Snyk/Trivy.
1210
+ 4. Docker images must be tagged with commit SHA.
1211
+ 5. Production deployment requires manual approval.
1212
+ 6. Database migrations must run before the app starts (if applicable).
1213
+
1214
+ ---
1215
+
1216
+ ## 12. Deployment Standards
1217
+
1218
+ ### 12.1 Multi-Stage Dockerfile
1219
+
1220
+ ```dockerfile
1221
+ FROM node:20-alpine AS deps
1222
+ WORKDIR /app
1223
+ COPY package.json pnpm-lock.yaml ./
1224
+ RUN pnpm install --frozen-lockfile
1225
+
1226
+ FROM node:20-alpine AS builder
1227
+ WORKDIR /app
1228
+ COPY --from=deps /app/node_modules ./node_modules
1229
+ COPY . .
1230
+ RUN pnpm build
1231
+
1232
+ FROM node:20-alpine AS runner
1233
+ WORKDIR /app
1234
+ ENV NODE_ENV=production
1235
+ RUN addgroup --system --gid 1001 nodejs
1236
+ RUN adduser --system --uid 1001 appuser
1237
+ COPY --from=builder /app/dist ./dist
1238
+ COPY --from=builder /app/package.json ./
1239
+ COPY --from=builder /app/node_modules ./node_modules
1240
+ USER appuser
1241
+ EXPOSE 3000
1242
+ CMD ["node", "dist/server.js"]
1243
+ ```
1244
+
1245
+ ### 12.2 Environment Strategy
1246
+
1247
+ | Environment | Purpose | Deploy Trigger | Replicas |
1248
+ |-------------|---------|---------------|----------|
1249
+ | Dev | Developer testing | Push to develop | 1 |
1250
+ | Staging | QA + UAT | Push to main | 2 |
1251
+ | Production | Live | Manual approval | 3+ (auto-scale) |
1252
+
1253
+ ### 12.3 Deployment Checklist
1254
+
1255
+ - [ ] Environment variables configured in secret manager (not committed)
1256
+ - [ ] Health check endpoint implemented (/health)
1257
+ - [ ] Readiness probe configured (Kubernetes)
1258
+ - [ ] Resource limits set (CPU/Memory)
1259
+ - [ ] Database migration plan prepared (if applicable)
1260
+ - [ ] Rollback plan documented
1261
+ - [ ] Monitoring alerts configured
1262
+ - [ ] Smoke tests passing
1263
+
1264
+ ---
1265
+
1266
+ ## 13. Security Standards
1267
+
1268
+ ### 13.1 Mandatory Security Measures
1269
+
1270
+ | Measure | Implementation | Priority |
1271
+ |---------|---------------|----------|
1272
+ | HTTPS | Enforced at ingress level | Critical |
1273
+ | CSP Headers | Content Security Policy via server headers | Critical |
1274
+ | Input Validation | Zod schemas for all user inputs | Critical |
1275
+ | XSS Protection | React escaping + DOMPurify for raw HTML | Critical |
1276
+ | CSRF Protection | SameSite cookies + CSRF tokens | High |
1277
+ | Rate Limiting | API endpoints (especially auth) | High |
1278
+ | Dependency Scanning | npm audit / Snyk in CI | High |
1279
+ | Secret Scanning | GitLeaks / TruffleHog in CI | Medium |
1280
+ | CORS | Whitelist allowed origins | High |
1281
+
1282
+ ### 13.2 Secrets NEVER Committed
1283
+
1284
+ - API keys, tokens, passwords
1285
+ - JWT signing secrets
1286
+ - Database connection strings
1287
+ - Cloud provider credentials
1288
+ - SSL/TLS private keys
1289
+ - Any .pem file
1290
+
1291
+ ### 13.3 .env.example Template
1292
+
1293
+ ```bash
1294
+ # App
1295
+ VITE_APP_TITLE=My App
1296
+ VITE_API_URL=http://localhost:3000/api/v1
1297
+
1298
+ # Auth (if applicable)
1299
+ VITE_AUTH_DOMAIN=your-domain.auth0.com
1300
+ VITE_AUTH_CLIENT_ID=your-client-id
1301
+
1302
+ # Monitoring
1303
+ VITE_SENTRY_DSN=https://xxx@sentry.io/xxx
1304
+
1305
+ # Feature Flags
1306
+ VITE_ENABLE_DARK_MODE=true
1307
+ VITE_ENABLE_NEW_DASHBOARD=false
1308
+ ```
1309
+
1310
+ ---
1311
+
1312
+ ### 13.4 Console.Log & Debugger Enforcement
1313
+
1314
+ Console and debugger rules: `console.log`, `console.debug`, `console.info`, `console.trace`, `console.table`, and `debugger` are banned in committed code.
1315
+
1316
+ Allowed only: `console.warn()` and `console.error()`. Use a structured logger instead.
1317
+
1318
+ ESLint rules:
1319
+ ```
1320
+ 'no-console': ['error', { allow: ['warn', 'error'] }],
1321
+ 'no-debugger': 'error',
1322
+ ```
1323
+
1324
+ Structured logger replacement:
1325
+ ```typescript
1326
+ const isDev = import.meta.env.DEV;
1327
+
1328
+ export const logger = {
1329
+ debug: (...args: unknown[]) => { if (isDev) console.debug('[DEBUG]', ...args); },
1330
+ warn: (...args: unknown[]) => console.warn('[WARN]', ...args),
1331
+ error: (...args: unknown[]) => console.error('[ERROR]', ...args),
1332
+ };
1333
+ ```
1334
+
1335
+ Production stripping (Vite/Terser):
1336
+ ```typescript
1337
+ build: {
1338
+ minify: 'terser',
1339
+ terserOptions: {
1340
+ compress: {
1341
+ drop_console: true,
1342
+ drop_debugger: true,
1343
+ },
1344
+ },
1345
+ }
1346
+ ```
1347
+
1348
+ ---
1349
+
1350
+ ---
1351
+
1352
+ ### 13.5 API Response Masking (Network Tab Security)
1353
+
1354
+ Sensitive fields must NEVER appear in network responses visible in the browser Network tab.
1355
+
1356
+ Mask these data types:
1357
+ - Auth tokens (JWT, session IDs)
1358
+ - Passwords, password hashes, salts
1359
+ - PII (SSN, phone, address, email)
1360
+ - Financial data (card numbers, salaries)
1361
+ - Internal IDs (database IDs, internal references)
1362
+ - API keys, secrets, private keys
1363
+
1364
+ Strategy 1: Use public DTOs in service layer so the backend only returns safe fields.
1365
+
1366
+ Strategy 2: Global response interceptor masking sensitive fields.
1367
+ ```typescript
1368
+ class ApiClient {
1369
+ private SENSITIVE_PATTERNS = [
1370
+ 'password', 'passwordhash', 'salt',
1371
+ 'ssn', 'socialsecurity', 'nationalid',
1372
+ 'internalid', 'databaseid', 'legacyid',
1373
+ 'creditcardnumber', 'cvv', 'bankaccount',
1374
+ 'apikey', 'privatekey', 'token', 'sessionid',
1375
+ 'salary', 'income', 'debt', 'revenue',
1376
+ ];
1377
+
1378
+ constructor() {
1379
+ this.client.interceptors.response.use((response) => ({
1380
+ ...response,
1381
+ data: this.maskSensitiveData(response.data),
1382
+ }));
1383
+ }
1384
+
1385
+ private maskSensitiveData(data: unknown): unknown {
1386
+ if (typeof data !== 'object' || data === null) return data;
1387
+ if (Array.isArray(data)) return data.map(item => this.maskSensitiveData(item));
1388
+ const masked: Record<string, unknown> = {};
1389
+ for (const [key, value] of Object.entries(data)) {
1390
+ const lowerKey = key.toLowerCase();
1391
+ if (this.SENSITIVE_PATTERNS.some(p => lowerKey.includes(p))) {
1392
+ masked[key] = '[REDACTED]';
1393
+ } else if (typeof value === 'object' && value !== null) {
1394
+ masked[key] = this.maskSensitiveData(value);
1395
+ } else {
1396
+ masked[key] = value;
1397
+ }
1398
+ }
1399
+ return masked;
1400
+ }
1401
+ }
1402
+ ```
1403
+
1404
+ Strategy 3: Disable caching for sensitive endpoints.
1405
+ ```typescript
1406
+ export const sensitiveService = {
1407
+ getFinancialData: () =>
1408
+ apiClient.get('/reports/financial', {
1409
+ headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' },
1410
+ }),
1411
+ };
1412
+ ```
1413
+
1414
+ Production verification checklist:
1415
+ - Open DevTools > Network tab
1416
+ - Verify no Authorization header visible (use httpOnly cookies)
1417
+ - Verify no passwords, SSNs, or internal IDs in request bodies
1418
+ - Verify masked responses show [REDACTED] for sensitive fields
1419
+
1420
+
1421
+ ### 13.6 Authorization: RBAC, ABAC, Guards, and DevMode Safety
1422
+
1423
+ Required authorization model: RBAC baseline, ABAC for policy restrictions. Enforced in route guards, component guards, and data/service guards. Backend must also enforce policy independently.
1424
+
1425
+ Core rules:
1426
+ - No feature, page, action, button, table row, or API call may be rendered or executed without an authorization check
1427
+ - Authorized state MUST NOT be derivable from the UI alone in developer mode
1428
+ - Authorization checks MUST be centralized
1429
+
1430
+ RBAC structure:
1431
+ ```tsx
1432
+ export enum Role {
1433
+ SUPER_ADMIN = 'super_admin',
1434
+ TENANT_ADMIN = 'tenant_admin',
1435
+ MANAGER = 'manager',
1436
+ ANALYST = 'analyst',
1437
+ }
1438
+
1439
+ export enum Permission {
1440
+ USERS_READ = 'users:read',
1441
+ USERS_WRITE = 'users:write',
1442
+ USERS_DELETE = 'users:delete',
1443
+ REPORTS_READ = 'reports:read',
1444
+ BILLING_READ = 'billing:read',
1445
+ BILLING_WRITE = 'billing:write',
1446
+ SETTINGS_WRITE = 'settings:write',
1447
+ }
1448
+ ```
1449
+
1450
+ ABAC structure:
1451
+ ```tsx
1452
+ export type AbacContext = {
1453
+ role: Role;
1454
+ tenantId: string;
1455
+ userId: string;
1456
+ ownershipIds: string[];
1457
+ region: string;
1458
+ clearanceLevel: number;
1459
+ isImpersonating: boolean;
1460
+ };
1461
+
1462
+ export type AbacPolicy = (
1463
+ ctx: AbacContext,
1464
+ action: string,
1465
+ resource: { type: string; id?: string; ownerId?: string; tenantId?: string }
1466
+ ) => boolean;
1467
+
1468
+ export const canAccessResource: AbacPolicy = (ctx, action, resource) => {
1469
+ if (ctx.isImpersonating) return false;
1470
+ if (resource.tenantId && resource.tenantId !== ctx.tenantId) return false;
1471
+ if (resource.ownerId && !ctx.ownershipIds.includes(resource.ownerId)) return false;
1472
+ if (action.startsWith('admin:') && ctx.role !== Role.SUPER_ADMIN) return false;
1473
+ return true;
1474
+ };
1475
+ ```
1476
+
1477
+ Route guards:
1478
+ ```tsx
1479
+ export function RequireAuth({ roles, permissions, abacCheck, children }: RequireAuthProps) {
1480
+ const auth = useAuth();
1481
+ const location = useLocation();
1482
+ if (!auth.isAuthenticated) return <Navigate to='/login' state={{ from: location }} replace />;
1483
+ const hasRole = roles ? roles.includes(auth.role) : true;
1484
+ const hasPermission = permissions ? permissions.every(p => auth.permissions.includes(p)) : true;
1485
+ const abacPass = abacCheck ? abacCheck(auth.abacContext) : true;
1486
+ if (!hasRole || !hasPermission || !abacPass) return <Forbidden />;
1487
+ return <>{children}</>;
1488
+ }
1489
+ ```
1490
+
1491
+ Component guards:
1492
+ ```tsx
1493
+ export function Can({ roles, permissions, abacCheck, fallback = null, children }: CanProps) {
1494
+ const auth = useAuth();
1495
+ const hasRole = roles ? roles.includes(auth.role) : true;
1496
+ const hasPermission = permissions ? permissions.some(p => auth.permissions.includes(p)) : true;
1497
+ const abacPass = abacCheck ? abacCheck(auth.abacContext) : true;
1498
+ if (!hasRole || !hasPermission || !abacPass) return <>{fallback}</>;
1499
+ return <>{children}</>;
1500
+ }
1501
+ ```
1502
+
1503
+ Data guards:
1504
+ ```tsx
1505
+ export function useAuthorizedUser(id: string) {
1506
+ const auth = useAuth();
1507
+ return useQuery({
1508
+ queryKey: ['user', id],
1509
+ queryFn: async () => {
1510
+ const data = await usersService.getById(id);
1511
+ if (!canAccessResource(auth.abacContext, 'users:read', { type: 'user', id, tenantId: data.tenantId })) {
1512
+ throw new Error('Forbidden');
1513
+ }
1514
+ return data;
1515
+ },
1516
+ });
1517
+ }
1518
+ ```
1519
+
1520
+ DevMode safety:
1521
+ - Dev-only endpoints, hidden buttons, debug flags MUST NOT bypass authorization
1522
+ - Hardcoded ADMIN=true query params, secret URL paths MUST be treated as security bugs
1523
+ - Impersonation and elevated roles MUST be logged and auditable
1524
+
1525
+ ESLint enforcement:
1526
+ ```
1527
+ no-restricted-syntax: ['error', {
1528
+ selector: "MemberExpression[property.name='role']",
1529
+ message: 'Use <Can> or useAuthorized* guards instead of inline role checks.',
1530
+ }]
1531
+ ```
1532
+
1533
+ Authorization code review checklist:
1534
+ - Every protected route uses RequireAuth
1535
+ - Every sensitive action uses Can
1536
+ - ABAC policies for tenant, ownership, region, or time restrictions
1537
+ - No inline role/permission checks in JSX
1538
+ - No hidden dev paths bypassing authorization
1539
+ - No mock authorization used outside tests
1540
+ ## 14. Monitoring & Observability
1541
+
1542
+ ### 14.1 Required Instrumentation
1543
+
1544
+ | Tool | Purpose | Implementation |
1545
+ |------|---------|---------------|
1546
+ | Sentry | Error tracking (frontend) | @sentry/react |
1547
+ | Lighthouse CI | Performance budget | GitHub Action |
1548
+ | Console Logging | Structured logging | No console.log in production |
1549
+
1550
+ ### 14.2 Performance Budget
1551
+
1552
+ | Metric | Budget | Enforcement |
1553
+ |--------|--------|-------------|
1554
+ | First Contentful Paint (FCP) | < 1.5s | Lighthouse CI |
1555
+ | Largest Contentful Paint (LCP) | < 2.5s | Lighthouse CI |
1556
+ | Time to Interactive (TTI) | < 3.5s | Lighthouse CI |
1557
+ | Bundle Size (initial) | < 200KB gzipped | size-limit package |
1558
+
1559
+ ---
1560
+
1561
+ ## 15. Documentation Standards
1562
+
1563
+ ### 15.1 Minimum Required Docs
1564
+
1565
+ | File | Purpose | Required |
1566
+ |------|---------|----------|
1567
+ | README.md | Project overview, setup, commands | Yes |
1568
+ | CONTRIBUTING.md | How to contribute, coding standards | Yes |
1569
+ | .env.example | All required environment variables | Yes |
1570
+ | docs/architecture.md | High-level architecture decisions | For complex projects |
1571
+ | docs/onboarding.md | Setup checklist for new developers | For monorepos |
1572
+
1573
+ ### 15.2 README Template
1574
+
1575
+ ```markdown
1576
+ # Project Name
1577
+ [Brief description]
1578
+
1579
+ ## Quick Start
1580
+ ```bash
1581
+ git clone <repo>
1582
+ cd project
1583
+ pnpm install
1584
+ cp .env.example .env
1585
+ pnpm dev
1586
+ ```
1587
+
1588
+ ## Available Scripts
1589
+ | Command | Description |
1590
+ | pnpm dev | Start development server |
1591
+ | pnpm build | Build for production |
1592
+ | pnpm test | Run tests |
1593
+ | pnpm lint | Lint code |
1594
+
1595
+ ## Environment Variables
1596
+ [Table of required env vars]
1597
+ ```
1598
+
1599
+ ---
1600
+
1601
+ ## 16. Appendix: Quick References
1602
+
1603
+ ### 16.1 Essential Scripts
1604
+
1605
+ ```bash
1606
+ pnpm dev # Start dev server with HMR
1607
+ pnpm build # Production build
1608
+ pnpm test # Run unit tests
1609
+ pnpm test:e2e # Run E2E tests
1610
+ pnpm lint # Lint all files
1611
+ pnpm format # Format with Prettier
1612
+ pnpm check-types # TypeScript type checking
1613
+ pnpm audit # Security audit
1614
+ ```
1615
+
1616
+ ### 16.2 File Naming Reference
1617
+
1618
+ ```
1619
+ UserProfile.tsx # React component (PascalCase)
1620
+ use-user-data.ts # Custom hook (kebab-case)
1621
+ user-service.ts # Service/utility (kebab-case)
1622
+ UserProfile.test.tsx # Test file (matches component)
1623
+ UserProfile.stories.tsx # Storybook file (matches component)
1624
+ types/user.ts # Type definitions
1625
+ constants/auth.ts # Constants
1626
+ ```
1627
+
1628
+ ### 16.3 Key Config Files
1629
+
1630
+ | File | Purpose |
1631
+ |------|---------|
1632
+ | tsconfig.json | TypeScript configuration |
1633
+ | vite.config.ts | Vite build configuration |
1634
+ | eslint.config.js | ESLint configuration |
1635
+ | .prettierrc | Prettier formatting rules |
1636
+ | .husky/pre-commit | Pre-commit hook (lint-staged) |
1637
+ | commitlint.config.js | Conventional commit rules |
1638
+ | .github/workflows/ci.yml | CI pipeline |
1639
+ | .github/workflows/cd.yml | CD pipeline |
1640
+ | Dockerfile | Container build |
1641
+ | docker-compose.yml | Local development |
1642
+ | .env.example | Documented env variables |
1643
+
1644
+ ### 16.4 Package.json Scripts Template
1645
+
1646
+ ```json
1647
+ {
1648
+ "scripts": {
1649
+ "dev": "vite",
1650
+ "build": "tsc --noEmit && vite build",
1651
+ "preview": "vite preview",
1652
+ "test": "vitest run",
1653
+ "test:watch": "vitest",
1654
+ "test:ci": "vitest run --coverage",
1655
+ "test:e2e": "playwright test",
1656
+ "lint": "eslint src/ --ext .ts,.tsx --max-warnings 0",
1657
+ "format": "prettier --write src/**/*.{ts,tsx,css,json}",
1658
+ "format:check": "prettier --check src/**/*.{ts,tsx,css,json}",
1659
+ "check-types": "tsc --noEmit",
1660
+ "prepare": "husky",
1661
+ "audit": "pnpm audit --audit-level=high"
1662
+ }
1663
+ }
1664
+ ```
1665
+
1666
+ ---
1667
+
1668
+ ---
1669
+
1670
+ ## 17. API Service Layer Standards
1671
+
1672
+ ### 17.1 Why a Services Layer?
1673
+
1674
+ A dedicated **API service layer** separates all backend communication from UI components. This is critical for:
1675
+
1676
+ - **Consistency**: All API calls go through the same error handling, auth headers, and response parsing
1677
+ - **Testability**: Service functions can be mocked independently of components
1678
+ - **Caching**: Centralized caching and deduplication of requests
1679
+ - **Performance**: Request batching, debouncing, and cancellation handled in one place
1680
+ - **Maintainability**: Backend URL changes require one file change, not hundreds
1681
+
1682
+ ### 17.2 Recommended Architecture
1683
+
1684
+ ```
1685
+ src/
1686
+ services/ # API service layer (domain-modularized)
1687
+ api-client.ts # Axios/fetch instance with interceptors (ONLY file at root)
1688
+ auth/ # Auth domain
1689
+ index.ts # Barrel: re-exports all auth services
1690
+ auth.service.ts # Auth-related API calls
1691
+ users/ # Users domain
1692
+ index.ts # Barrel: re-exports all user services
1693
+ users.service.ts # User-related API calls
1694
+ employee/ # Employee domain
1695
+ index.ts # Barrel: re-exports all employee services
1696
+ employee-bio-detail.service.ts # Bio detail API calls
1697
+ employee-bank-detail.service.ts# Bank detail API calls
1698
+ employee-leave.service.ts # Leave management API calls
1699
+ hooks/ # React hooks that consume services
1700
+ use-debounce.ts # Utility hook (shared, allowed at root)
1701
+ users/ # User domain hooks
1702
+ index.ts # Barrel
1703
+ use-users.ts # User data hook using React Query + service
1704
+ employee/ # Employee domain hooks
1705
+ index.ts # Barrel
1706
+ use-employee-bio.ts # Employee bio hook
1707
+ components/ # UI components (no direct API calls)
1708
+ ```
1709
+
1710
+ ### 17.3 API Client Template
1711
+
1712
+ ```typescript
1713
+ import axios, { AxiosError, AxiosInstance } from 'axios';
1714
+
1715
+ const API_BASE_URL = import.meta.env.VITE_API_URL || '/api/v1';
1716
+
1717
+ class ApiClient {
1718
+ private client: AxiosInstance;
1719
+
1720
+ constructor() {
1721
+ this.client = axios.create({
1722
+ baseURL: API_BASE_URL,
1723
+ timeout: 30000,
1724
+ headers: { 'Content-Type': 'application/json' },
1725
+ });
1726
+ this.setupInterceptors();
1727
+ }
1728
+
1729
+ private setupInterceptors() {
1730
+ this.client.interceptors.request.use((config) => {
1731
+ const token = getAccessToken();
1732
+ if (token) config.headers.Authorization = `Bearer ${token}`;
1733
+ return config;
1734
+ });
1735
+
1736
+ this.client.interceptors.response.use(
1737
+ (res) => res,
1738
+ (error: AxiosError) => {
1739
+ if (error.response?.status === 401) window.location.href = '/login';
1740
+ return Promise.reject(this.normalizeError(error));
1741
+ }
1742
+ );
1743
+ }
1744
+
1745
+ private normalizeError(error: AxiosError) {
1746
+ return {
1747
+ message: error.response?.data?.message || error.message,
1748
+ status: error.response?.status || 500,
1749
+ code: error.response?.data?.code || 'UNKNOWN_ERROR',
1750
+ };
1751
+ }
1752
+
1753
+ async get<T>(url: string): Promise<T> {
1754
+ const res = await this.client.get<T>(url);
1755
+ return res.data;
1756
+ }
1757
+ async post<T>(url: string, data?: unknown): Promise<T> {
1758
+ const res = await this.client.post<T>(url, data);
1759
+ return res.data;
1760
+ }
1761
+ async put<T>(url: string, data?: unknown): Promise<T> {
1762
+ const res = await this.client.put<T>(url, data);
1763
+ return res.data;
1764
+ }
1765
+ async delete<T>(url: string): Promise<T> {
1766
+ const res = await this.client.delete<T>(url);
1767
+ return res.data;
1768
+ }
1769
+ }
1770
+
1771
+ export const apiClient = new ApiClient();
1772
+ ```
1773
+
1774
+ ### 17.4 Service Module Template (Domain-Modularized with Barrel Export)
1775
+
1776
+ Each service file lives inside its domain subdirectory and is re-exported via the barrel `index.ts`.
1777
+
1778
+ ```typescript
1779
+ // src/services/users/users.service.ts
1780
+ import { apiClient } from '@/services/api-client';
1781
+ import type { User, PaginatedResponse } from '@/types';
1782
+
1783
+ const ENDPOINT = '/users';
1784
+
1785
+ export const usersService = {
1786
+ getAll: (page = 1, limit = 20) =>
1787
+ apiClient.get<PaginatedResponse<User>>(`${ENDPOINT}?page=${page}&limit=${limit}`),
1788
+ getById: (id: string) => apiClient.get<User>(`${ENDPOINT}/${id}`),
1789
+ create: (data: Partial<User>) => apiClient.post<User>(ENDPOINT, data),
1790
+ update: (id: string, data: Partial<User>) => apiClient.put<User>(`${ENDPOINT}/${id}`, data),
1791
+ delete: (id: string) => apiClient.delete<void>(`${ENDPOINT}/${id}`),
1792
+ };
1793
+ ```
1794
+
1795
+ ```typescript
1796
+ // src/services/users/index.ts (BARREL FILE -- REQUIRED)
1797
+ export { usersService } from './users.service';
1798
+ ```
1799
+
1800
+ ```typescript
1801
+ // src/services/employee/employee-bio-detail.service.ts
1802
+ import { apiClient } from '@/services/api-client';
1803
+ import type { EmployeeBio } from '@/types';
1804
+
1805
+ const ENDPOINT = '/employees';
1806
+
1807
+ export const employeeBioDetailService = {
1808
+ getBio: (employeeId: string) =>
1809
+ apiClient.get<EmployeeBio>(`${ENDPOINT}/${employeeId}/bio`),
1810
+ updateBio: (employeeId: string, data: Partial<EmployeeBio>) =>
1811
+ apiClient.put<EmployeeBio>(`${ENDPOINT}/${employeeId}/bio`, data),
1812
+ };
1813
+ ```
1814
+
1815
+ ```typescript
1816
+ // src/services/employee/index.ts (BARREL FILE -- REQUIRED)
1817
+ export { employeeBioDetailService } from './employee-bio-detail.service';
1818
+ export { employeeBankDetailService } from './employee-bank-detail.service';
1819
+ export { employeeLeaveService } from './employee-leave.service';
1820
+ ```
1821
+
1822
+ **Consumer imports** (pages/components import from the barrel only):
1823
+
1824
+ ```typescript
1825
+ // pages/EmployeeProfile.tsx -- GOOD (barrel import)
1826
+ import { employeeBioDetailService } from '@/services/employee';
1827
+ import { usersService } from '@/services/users';
1828
+
1829
+ // pages/EmployeeProfile.tsx -- BAD (deep import -- BLOCKED by ESLint)
1830
+ import { employeeBioDetailService } from '@/services/employee/employee-bio-detail.service';
1831
+ ```
1832
+
1833
+ ### 17.5 Hook That Consumes a Service (With React Query + Barrel Imports)
1834
+
1835
+ ```typescript
1836
+ // src/hooks/users/use-users.ts
1837
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
1838
+ import { usersService } from '@/services/users';
1839
+ // NOT: import { usersService } from '@/services/users/users.service'; // BLOCKED
1840
+
1841
+ // Query key factory for consistent cache management
1842
+ export const userKeys = {
1843
+ all: ['users'] as const,
1844
+ lists: () => [...userKeys.all, 'list'] as const,
1845
+ list: (filters: object) => [...userKeys.lists(), filters] as const,
1846
+ details: () => [...userKeys.all, 'detail'] as const,
1847
+ detail: (id: string) => [...userKeys.details(), id] as const,
1848
+ };
1849
+
1850
+ // Fetch paginated users (auto-cached + stale-while-revalidate)
1851
+ export function useUsers(page = 1) {
1852
+ return useQuery({
1853
+ queryKey: userKeys.list({ page }),
1854
+ queryFn: () => usersService.getAll(page),
1855
+ staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
1856
+ gcTime: 30 * 60 * 1000, // Keep cache for 30 minutes
1857
+ refetchOnWindowFocus: true,
1858
+ retry: 3,
1859
+ });
1860
+ }
1861
+
1862
+ // Create user with automatic cache invalidation
1863
+ export function useCreateUser() {
1864
+ const queryClient = useQueryClient();
1865
+ return useMutation({
1866
+ mutationFn: usersService.create,
1867
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: userKeys.lists() }),
1868
+ });
1869
+ }
1870
+ ```
1871
+
1872
+ ### 17.6 Client-Side Caching Strategy
1873
+
1874
+ | Cache Layer | Technology | Use Case | Persistence |
1875
+ |-------------|-----------|----------|-------------|
1876
+ | Server State | TanStack React Query | API caching, dedup, background refetch | In-memory |
1877
+ | Persistent Storage | localStorage | User preferences, theme, auth tokens | Disk |
1878
+ | Large Data | IndexedDB via Dexie.js | Offline data, large datasets | Disk |
1879
+ | Route Prefetch | Router loaders | Anticipatory data loading | In-memory |
1880
+ | Service Worker | Workbox | Full offline support | Cache Storage API |
1881
+
1882
+ **Rules**:
1883
+ - Never cache sensitive data (tokens, PII) in localStorage without encryption
1884
+ - Always set staleTime / gcTime for API responses
1885
+ - Invalidate caches on mutations (create, update, delete)
1886
+ - Prefer React Query over manual fetch + useState for server data
1887
+ - Use React Query staleTime to control refetch frequency, not arbitrary timers
1888
+
1889
+ ---
1890
+
1891
+ ## 18. React Hooks Best Practices
1892
+
1893
+ ### 18.1 The Golden Rule
1894
+
1895
+ > **Hooks are for behavior, not data.** If data comes from an API, use React Query + your service layer. Reserve hooks for:
1896
+ > - Connecting UI to data (React Query hooks)
1897
+ > - Encapsulating complex browser APIs (resize, scroll, geolocation)
1898
+ > - Reusable form logic
1899
+ > - Performance optimization (useMemo, useCallback)
1900
+
1901
+ ### 18.2 useEffect -- When and How
1902
+
1903
+ **DO use useEffect for**:
1904
+ - Synchronizing with external systems (WebSocket, browser APIs, analytics)
1905
+ - Setting up subscriptions with proper cleanup
1906
+ - Managing non-React state (syncing to localStorage)
1907
+ - Imperative operations (focus management, scroll restoration)
1908
+
1909
+ **DO NOT use useEffect for**:
1910
+ - Fetching data on mount --> Use React Query
1911
+ - Deriving state from props --> Use useMemo
1912
+ - Responding to user actions --> Use event handlers directly
1913
+ - Computed values --> Use useMemo or inline computation
1914
+
1915
+ ```typescript
1916
+ // GOOD: Clean subscription with cleanup
1917
+ useEffect(() => {
1918
+ const socket = new WebSocket(url);
1919
+ socket.onmessage = (event) => setMessages((prev) => [...prev, JSON.parse(event.data)]);
1920
+ return () => socket.close(); // Cleanup on unmount
1921
+ }, [url]);
1922
+
1923
+ // BAD: Fetching data manually (use React Query instead)
1924
+ useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers); }, []);
1925
+
1926
+ // BAD: Deriving state from props
1927
+ useEffect(() => { setFullName(`${firstName} ${lastName}`); }, [firstName, lastName]);
1928
+ // FIX: const fullName = `${firstName} ${lastName}`;
1929
+ ```
1930
+
1931
+ ### 18.3 useMemo -- When to Memoize
1932
+
1933
+ **DO use useMemo for**:
1934
+ - Expensive computations (filtering/sorting large lists, complex data transformation)
1935
+ - Creating stable object references for child component props (prevents re-renders)
1936
+
1937
+ **DO NOT use useMemo for**:
1938
+ - Simple calculations (firstName + lastName)
1939
+ - Values used once or in a single render
1940
+ - Premature optimization -- measure first, then memoize
1941
+
1942
+ ```typescript
1943
+ // GOOD: Expensive computation memoized
1944
+ const filteredUsers = useMemo(() => {
1945
+ return users
1946
+ .filter((u) => u.role === selectedRole)
1947
+ .sort((a, b) => a.name.localeCompare(b.name));
1948
+ }, [users, selectedRole]);
1949
+
1950
+ // GOOD: Stable object reference for child
1951
+ const config = useMemo(() => ({ pageSize: 20, enableSorting: true }), []);
1952
+
1953
+ // BAD: Simple operation -- adds overhead
1954
+ const greeting = useMemo(() => `Hello, ${name}!`, [name]);
1955
+ // Just write: const greeting = `Hello, ${name}!`;
1956
+ ```
1957
+
1958
+ ### 18.4 useCallback -- When to Memoize Functions
1959
+
1960
+ **DO use useCallback for**:
1961
+ - Callback props passed to memoized children (React.memo)
1962
+ - Functions in useEffect dependency arrays
1963
+ - Functions passed to context providers
1964
+
1965
+ **DO NOT use useCallback for**:
1966
+ - Event handlers on native HTML elements (<button onClick>)
1967
+ - Functions used in a single parent component with no memoized children
1968
+ - Premature optimization
1969
+
1970
+ ```typescript
1971
+ // GOOD: Stable callback for memoized child
1972
+ const Table = React.memo(({ onRowClick, data }) => { ... });
1973
+ function Parent() {
1974
+ const onRowClick = useCallback((id: string) => handleSelect(id), [handleSelect]);
1975
+ return <Table onRowClick={onRowClick} data={data} />;
1976
+ }
1977
+
1978
+ // BAD: Unnecessary -- native elements don't check prop equality
1979
+ const handleClick = useCallback(() => setCount((c) => c + 1), []);
1980
+ // <button onClick={handleClick}> -- NO BENEFIT
1981
+
1982
+ // BAD: useCallback + inline function = same as no memo
1983
+ const handleSubmit = useCallback(() => { submit(data); }, [data]);
1984
+ // Same as: const handleSubmit = () => { submit(data); };
1985
+ ```
1986
+
1987
+ ### 18.5 useTransition & useDeferredValue -- For Responsive UIs
1988
+
1989
+ These hooks prevent the UI from freezing during expensive state updates.
1990
+
1991
+ ```typescript
1992
+ // useTransition: Mark a state update as non-urgent
1993
+ function SearchPage() {
1994
+ const [isPending, startTransition] = useTransition();
1995
+ const [query, setQuery] = useState('');
1996
+ const [results, setResults] = useState([]);
1997
+
1998
+ const handleChange = (e) => {
1999
+ const value = e.target.value;
2000
+ setQuery(value); // Urgent: input must feel responsive
2001
+ startTransition(() => {
2002
+ setResults(filterHugeList(value)); // Deferred: can wait
2003
+ });
2004
+ };
2005
+
2006
+ return (
2007
+ <div>
2008
+ <input value={query} onChange={handleChange} />
2009
+ {isPending ? <Spinner /> : <List items={results} />}
2010
+ </div>
2011
+ );
2012
+ }
2013
+
2014
+ // useDeferredValue: Defer re-render of non-critical content
2015
+ function Dashboard() {
2016
+ const [input, setInput] = useState('');
2017
+ const deferredInput = useDeferredValue(input);
2018
+ const isStale = input !== deferredInput;
2019
+
2020
+ return (
2021
+ <div>
2022
+ <input value={input} onChange={(e) => setInput(e.target.value)} />
2023
+ <SlowList text={deferredInput} /> {/* Re-renders less often */}
2024
+ {isStale && <div>Updating...</div>}
2025
+ </div>
2026
+ );
2027
+ }
2028
+ ```
2029
+
2030
+ **When to use**:
2031
+ - Search fields with large result sets
2032
+ - Tab switching with heavy content
2033
+ - Chart/table rendering blocking the UI thread
2034
+ - Filtering/sorting lists >10,000 items
2035
+
2036
+ ### 18.6 Custom Hooks for Performance
2037
+
2038
+ ```typescript
2039
+ // hooks/use-debounce.ts -- Debounce rapidly changing values
2040
+ export function useDebounce<T>(value: T, delay = 300): T {
2041
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
2042
+ useEffect(() => {
2043
+ const timer = setTimeout(() => setDebouncedValue(value), delay);
2044
+ return () => clearTimeout(timer);
2045
+ }, [value, delay]);
2046
+ return debouncedValue;
2047
+ }
2048
+
2049
+ // Usage: Reduces API calls during rapid typing
2050
+ function SearchUsers() {
2051
+ const [search, setSearch] = useState('');
2052
+ const debouncedSearch = useDebounce(search, 400); // Wait 400ms after last keystroke
2053
+
2054
+ const { data, isLoading } = useQuery({
2055
+ queryKey: ['users', 'search', debouncedSearch],
2056
+ queryFn: () => usersService.search(debouncedSearch),
2057
+ enabled: debouncedSearch.length >= 2, // Only search when 2+ chars
2058
+ });
2059
+
2060
+ return (
2061
+ <div>
2062
+ <input value={search} onChange={(e) => setSearch(e.target.value)} />
2063
+ {isLoading ? <Spinner /> : <UserList users={data} />}
2064
+ </div>
2065
+ );
2066
+ }
2067
+ ```
2068
+
2069
+ ### 18.7 Performance Decision Guide
2070
+
2071
+ | Situation | Solution | Why |
2072
+ |-----------|----------|-----|
2073
+ | Fetching API data | React Query (useQuery) | Built-in caching, dedup, retry, stale-while-revalidate |
2074
+ | Mutating server data | React Query (useMutation) | Optimistic updates, cache invalidation, rollback |
2075
+ | Expensive calculation on props change | useMemo | Only re-computes when dependencies change |
2076
+ | Stable callback for memoized child | useCallback | Prevents unnecessary child re-renders |
2077
+ | Debouncing user input | Custom useDebounce hook | Reduces API calls during rapid typing |
2078
+ | Non-urgent state update | useTransition | Keeps UI responsive during heavy renders |
2079
+ | Deferring slow component re-render | useDeferredValue | Shows stale content while new renders |
2080
+ | Subscribing to browser API | useEffect with cleanup | Proper lifecycle management |
2081
+ | Form state | React Hook Form | Built-in validation, minimal re-renders |
2082
+ | Global state | Zustand | No providers, selective subscriptions |
2083
+
2084
+ ### 18.8 Common Anti-Patterns to Avoid
2085
+
2086
+ ```typescript
2087
+ // ANTI-PATTERN: State + useEffect for derived data
2088
+ const [fullName, setFullName] = useState('');
2089
+ useEffect(() => { setFullName(`${user.firstName} ${user.lastName}`); }, [user]);
2090
+ // FIX: const fullName = `${user.firstName} ${user.lastName}`;
2091
+
2092
+ // ANTI-PATTERN: Fetching in useEffect without cleanup
2093
+ useEffect(() => { fetch(url).then(setData); }, [url]);
2094
+ // FIX: Use React Query or AbortController
2095
+
2096
+ // ANTI-PATTERN: Object/array in useEffect deps (causes infinite loop)
2097
+ useEffect(() => { doSomething(); }, [{ id: user.id }]);
2098
+ // FIX: Use primitive deps: [user.id]
2099
+
2100
+ // ANTI-PATTERN: useCallback everywhere
2101
+ const handleChange = useCallback((e) => setValue(e.target.value), []);
2102
+ // FIX: const handleChange = (e) => setValue(e.target.value);
2103
+
2104
+ // ANTI-PATTERN: Nested ternaries in JSX
2105
+ return (<div>{isLoading ? <Spin/> : isError ? <Error/> : data.length === 0 ? <Empty/> : <List data={data}/>}</div>);
2106
+ // FIX: Extract into variables or early returns
2107
+
2108
+ // ANTI-PATTERN: Giant useEffect doing everything
2109
+ useEffect(() => {
2110
+ fetchUsers(); setupWebSocket(); initAnalytics(); setInterval(poll, 5000);
2111
+ return () => { cleanupAll(); };
2112
+ }, []);
2113
+ // FIX: Split into multiple focused useEffect calls
2114
+ ```
2115
+
2116
+ ### 18.9 Decision Flowchart
2117
+
2118
+ ```
2119
+ Is this server data (API call)?
2120
+ |-- YES --> React Query (useQuery / useMutation)
2121
+ NO
2122
+ |
2123
+ Is this a computed/derived value?
2124
+ |-- YES --> Is it expensive (>1ms)?
2125
+ | |-- YES --> useMemo
2126
+ | |-- NO --> Compute at render
2127
+ NO
2128
+ |
2129
+ Is this a function passed as prop to memoized child?
2130
+ |-- YES --> useCallback
2131
+ |-- NO --> Regular function
2132
+ NO
2133
+ |
2134
+ Is this a side effect (subscription, browser API)?
2135
+ |-- YES --> useEffect + cleanup
2136
+ NO
2137
+ |
2138
+ Is the UI freezing during state updates?
2139
+ |-- YES --> Is it urgent (input, button)?
2140
+ | |-- YES --> Keep urgent update, useTransition for non-urgent
2141
+ | |-- NO --> useDeferredValue
2142
+ NO
2143
+ |
2144
+ You probably don't need any hook. Write plain JS/TS.
2145
+ ```
2146
+ ### 18.10 Form Validation & Submission Standards
2147
+
2148
+ Every form must use React Hook Form with Zod validation. The schema must be co-located with the form component.
2149
+
2150
+ ```typescript
2151
+ // pages/employee/employee-bio-form.tsx
2152
+ import { useForm } from 'react-hook-form';
2153
+ import { zodResolver } from '@hookform/resolvers/zod';
2154
+ import { z } from 'zod';
2155
+
2156
+ const employeeBioSchema = z.object({
2157
+ firstName: z.string().min(1, 'First name is required').max(100),
2158
+ lastName: z.string().min(1, 'Last name is required').max(100),
2159
+ email: z.string().email('Invalid email format'),
2160
+ phone: z.string().regex(/^\+?[\d\s-]{7,15}$/, 'Invalid phone number').optional(),
2161
+ departmentId: z.string().uuid('Invalid department'),
2162
+ });
2163
+
2164
+ type EmployeeBioFormData = z.infer<typeof employeeBioSchema>;
2165
+
2166
+ export function EmployeeBioForm() {
2167
+ const { register, handleSubmit, formState: { errors, isSubmitting, isDirty }, setError, reset } = useForm<EmployeeBioFormData>({
2168
+ resolver: zodResolver(employeeBioSchema),
2169
+ defaultValues: { firstName: '', lastName: '', email: '', phone: '', departmentId: '' },
2170
+ });
2171
+
2172
+ const mutation = useUpdateEmployeeBio();
2173
+
2174
+ const onSubmit = async (data: EmployeeBioFormData) => {
2175
+ try {
2176
+ await mutation.mutateAsync(data);
2177
+ reset(data); // Reset dirty state after successful save
2178
+ toast.success('Bio updated successfully');
2179
+ } catch (err) {
2180
+ // Map server errors to form fields
2181
+ if (err.response?.data?.errors) {
2182
+ for (const { field, message } of err.response.data.errors) {
2183
+ setError(field as keyof EmployeeBioFormData, { message });
2184
+ }
2185
+ } else {
2186
+ toast.error('Failed to update bio. Please try again.');
2187
+ }
2188
+ }
2189
+ };
2190
+
2191
+ // Ensure required: unsaved changes warning
2192
+ useEffect(() => {
2193
+ const handler = (e: BeforeUnloadEvent) => { if (isDirty) e.preventDefault(); };
2194
+ window.addEventListener('beforeunload', handler);
2195
+ return () => window.removeEventListener('beforeunload', handler);
2196
+ }, [isDirty]);
2197
+
2198
+ return (
2199
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
2200
+ <div>
2201
+ <label htmlFor="firstName">First Name</label>
2202
+ <input id="firstName" {...register('firstName')} aria-describedby="firstName-error" />
2203
+ {errors.firstName && <p id="firstName-error" role="alert">{errors.firstName.message}</p>}
2204
+ </div>
2205
+ {/* ... other fields ... */}
2206
+ <button type="submit" disabled={isSubmitting || !isDirty}>
2207
+ {isSubmitting ? <Spinner /> : 'Save'}
2208
+ </button>
2209
+ </form>
2210
+ );
2211
+ }
2212
+ ```
2213
+
2214
+ **Rules**:
2215
+ - Zod schema co-located with form (not in a separate `types/` file)
2216
+ - Server errors mapped to field-level via `setError` (not shown as generic toast)
2217
+ - Unsaved changes warning via `beforeunload` when `isDirty`
2218
+ - Submit button disabled when `isSubmitting` (loading spinner) or `!isDirty`
2219
+ - All inputs have `<label>` with `htmlFor` matching input `id`
2220
+ - Error messages linked via `aria-describedby`
2221
+ - Multi-step forms: each step is a separate form section with shared `useForm` + `watch`
2222
+ - Optimistic updates preferred over form submission where applicable
2223
+
2224
+ ---
2225
+
2226
+ ## 19. Enforcement: Husky, Git Hooks & Automated Validation
2227
+
2228
+ ### 19.1 Core Principle: Automation Over Manual Review
2229
+
2230
+ No rule is valuable if it relies on humans remembering to check it. Every standard in this document must have automated enforcement.
2231
+
2232
+ **Note on Package Managers**: While developers are free to use any package manager (npm, yarn, bun) for local development, all automated enforcement and CI/CD pipelines are optimized for **pnpm**. Ensure `pnpm-lock.yaml` is kept up to date.
2233
+
2234
+ **Mandatory Enforcement Layers**:
2235
+
2236
+ | Layer | Tool | When | Purpose |
2237
+ |-------|------|------|---------|
2238
+ | L1: Editor | ESLint + Prettier | Every save | Immediate feedback while coding |
2239
+ | L2: Pre-commit Hook | Husky + lint-staged | Every `git commit` | Block bad code before it enters history |
2240
+ | L3: CI Pipeline | GitHub Actions | Every PR / push | Enforce before merge, across all contributors |
2241
+ | L4: Code Review | Human review | Every PR | Architecture, logic, edge cases automation cannot catch |
2242
+
2243
+ ### 19.2 Required Husky + lint-staged Setup
2244
+
2245
+ #### Step 1: Install dependencies
2246
+
2247
+ ```bash
2248
+ pnpm add -D husky lint-staged
2249
+ ```
2250
+
2251
+ #### Step 2: Enable Husky
2252
+
2253
+ ```json
2254
+ // package.json
2255
+ {
2256
+ "scripts": {
2257
+ "prepare": "husky"
2258
+ }
2259
+ }
2260
+ ```
2261
+
2262
+ Run: `pnpm prepare` -- this creates the `.husky/` directory.
2263
+
2264
+ #### Step 3: Create Pre-commit Hook
2265
+
2266
+ ```bash
2267
+ # .husky/pre-commit
2268
+ #!/usr/bin/env sh
2269
+ . "$(dirname -- "$0")/_/husky.sh"
2270
+
2271
+ echo "Running lint-staged..."
2272
+ pnpm exec lint-staged
2273
+ ```
2274
+
2275
+ #### Step 4: Create Commit-msg Hook
2276
+
2277
+ ```bash
2278
+ # .husky/commit-msg
2279
+ #!/usr/bin/env sh
2280
+ . "$(dirname -- "$0")/_/husky.sh"
2281
+
2282
+ echo "Validating commit message..."
2283
+ pnpm exec commitlint --edit "$1"
2284
+ ```
2285
+
2286
+ #### Step 5: Create Pre-push Hook
2287
+
2288
+ ```bash
2289
+ # .husky/pre-push
2290
+ #!/usr/bin/env sh
2291
+ . "$(dirname -- "$0")/_/husky.sh"
2292
+
2293
+ echo "Running quality checks..."
2294
+ pnpm lint && pnpm check-modularization && pnpm check-types && pnpm test:ci
2295
+ ```
2296
+
2297
+ The `check-modularization` script (see Step 7) validates folder structure and barrel file compliance before code reaches CI.
2298
+
2299
+ #### Step 6: Configure lint-staged
2300
+
2301
+ ```json
2302
+ // package.json
2303
+ {
2304
+ "lint-staged": {
2305
+ "src/**/*.{ts,tsx}": [
2306
+ "eslint --fix --max-warnings 0",
2307
+ "prettier --write"
2308
+ ],
2309
+ "src/**/*.{css,scss}": ["prettier --write"],
2310
+ "*.{json,md,yaml,yml}": ["prettier --write"]
2311
+ }
2312
+ }
2313
+ ```
2314
+
2315
+ #### Step 7: Add Modularization Validation Script
2316
+
2317
+ Add a `check-modularization` script to `package.json` that validates domain-folder structure and barrel file compliance:
2318
+
2319
+ ```json
2320
+ // package.json
2321
+ {
2322
+ "scripts": {
2323
+ "check-modularization": "node scripts/check-modularization.mjs",
2324
+ "lint": "eslint --ext .ts,.tsx --max-warnings 0 .",
2325
+ "lint:fix": "eslint --ext .ts,.tsx . --fix",
2326
+ "format": "prettier --write \"src/**/*.{ts,tsx,json,css,md}\"",
2327
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,json,css,md}\"",
2328
+ "check-types": "tsc --noEmit",
2329
+ "prepare": "husky",
2330
+ "validate": "pnpm lint && pnpm check-modularization && pnpm check-types",
2331
+ "test:validate": "pnpm test:ci --coverage"
2332
+ }
2333
+ }
2334
+ ```
2335
+
2336
+ #### Step 8: Anti-Bypass Enforcement (`--no-verify` Protection)
2337
+
2338
+ The `git commit --no-verify` (or `-n`) flag skips pre-commit and commit-msg hooks locally. This is a **policy violation** and must be caught in CI. To ensure checks persist regardless:
2339
+
2340
+ **1. Commit Message Validation in CI** — since `--no-verify` can skip `commitlint` locally, CI validates all PR commit messages:
2341
+
2342
+ ```yaml
2343
+ # .github/workflows/ci.yml (add this step BEFORE the lint step)
2344
+ - name: Validate Commit Messages (anti-bypass for --no-verify)
2345
+ run: |
2346
+ echo "Checking all commit messages in this PR for conventional commit format..."
2347
+ pnpm exec commitlint --from origin/${{ github.base_ref }} --to HEAD
2348
+ # Fails if ANY commit message in the PR violates conventional commit format
2349
+ ```
2350
+
2351
+ **2. Pre-commit Checks Duplicated in CI** — every check that pre-commit runs locally also runs in CI with zero tolerance:
2352
+
2353
+ ```yaml
2354
+ - name: Lint (catches --no-verify bypass)
2355
+ run: pnpm lint
2356
+ # Runs regardless of whether developer skipped hooks locally
2357
+
2358
+ - name: Check Modularization (catches --no-verify bypass)
2359
+ run: pnpm check-modularization
2360
+ # Runs regardless of whether developer skipped hooks locally
2361
+ ```
2362
+
2363
+ **3. ESLint Rule to Detect `--no-verify` Usage Pattern** — add a `no-restricted-syntax` rule that flags common bypass patterns in CI scripts or husky config tampering:
2364
+
2365
+ ```javascript
2366
+ // eslint.config.js
2367
+ rules: {
2368
+ // Flag any attempted husky bypass or hook removal
2369
+ 'no-restricted-properties': ['error', {
2370
+ object: 'process',
2371
+ property: 'exit',
2372
+ message: 'Do not use process.exit to bypass validation hooks.',
2373
+ }],
2374
+ }
2375
+ ```
2376
+
2377
+ **4. Branch Protection Rule** — require all PR status checks to pass before merge. Even if a developer uses `--no-verify` locally, the PR cannot be merged because CI will fail on lint/modularization/commit-message checks.
2378
+
2379
+ **The `--no-verify` policy**:
2380
+
2381
+ | Action | Consequence |
2382
+ |--------|-------------|
2383
+ | `git commit --no-verify` | Commit lands locally, but CI catches all skipped checks on push/PR |
2384
+ | `git push --no-verify` | Push succeeds, but CI pre-push checks run anyway; PR blocked if failing |
2385
+ | Tampering with `.husky/` files | Caught in code review; `.husky/` is committed and reviewed |
2386
+ | Removing hooks from `package.json` | CI `pnpm install` re-installs husky; hooks re-created |
2387
+
2388
+ > **TL;DR**: `--no-verify` delays validation from commit-time to CI-time. The checks still run and the PR still gets blocked. There is no path to merge that bypasses these standards.
2389
+
2390
+ Create the validation script at `scripts/check-modularization.mjs`:
2391
+
2392
+ ```javascript
2393
+ // scripts/check-modularization.mjs
2394
+ import { readdirSync, existsSync, statSync } from 'fs';
2395
+ import { join, dirname } from 'path';
2396
+ import { fileURLToPath } from 'url';
2397
+
2398
+ const __dirname = dirname(fileURLToPath(import.meta.url));
2399
+ const PROJECT_ROOT = join(__dirname, '..');
2400
+ const SRC = join(PROJECT_ROOT, 'src');
2401
+
2402
+ // Modularized directories that require subdirectory organization
2403
+ const MODULARIZED_DIRS = ['services', 'hooks', 'store', 'components', 'lib', 'types'];
2404
+
2405
+ // Files allowed at the root of each directory (everything else must be in subdirs)
2406
+ const ALLOWED_ROOT_FILES = {
2407
+ services: ['api-client.ts', 'api-client.tsx'],
2408
+ hooks: ['use-debounce.ts', 'use-media-query.ts', 'index.ts'],
2409
+ store: ['index.ts'],
2410
+ components: ['index.ts'],
2411
+ lib: ['index.ts'],
2412
+ types: ['index.ts'],
2413
+ };
2414
+
2415
+ // Required barrel file name
2416
+ const BARREL_FILE = 'index.ts';
2417
+
2418
+ let errors = 0;
2419
+ let warnings = 0;
2420
+
2421
+ function logError(msg) {
2422
+ console.error(` ❌ ERROR: ${msg}`);
2423
+ errors++;
2424
+ }
2425
+
2426
+ function logWarning(msg) {
2427
+ console.warn(` ⚠️ WARNING: ${msg}`);
2428
+ warnings++;
2429
+ }
2430
+
2431
+ // Helper: check if a path is a directory
2432
+ function isDirectory(path) {
2433
+ try { return statSync(path).isDirectory(); } catch { return false; }
2434
+ }
2435
+
2436
+ // Helper: check if a file is a TypeScript file
2437
+ function isTsFile(filename) {
2438
+ return /\.(ts|tsx)$/.test(filename) && !filename.endsWith('.test.ts') && !filename.endsWith('.test.tsx') && !filename.endsWith('.spec.ts') && !filename.endsWith('.spec.tsx');
2439
+ }
2440
+
2441
+ console.log('\n🔍 Checking modularization compliance...\n');
2442
+
2443
+ for (const dirName of MODULARIZED_DIRS) {
2444
+ const dirPath = join(SRC, dirName);
2445
+ if (!existsSync(dirPath) || !isDirectory(dirPath)) continue;
2446
+
2447
+ const allowedRoot = ALLOWED_ROOT_FILES[dirName] || [];
2448
+ const entries = readdirSync(dirPath);
2449
+
2450
+ for (const entry of entries) {
2451
+ const entryPath = join(dirPath, entry);
2452
+
2453
+ if (isDirectory(entryPath)) {
2454
+ // This is a domain subdirectory -- check it has a barrel file
2455
+ console.log(` 📁 ${dirName}/${entry}/`);
2456
+
2457
+ const barrelPath = join(entryPath, BARREL_FILE);
2458
+ if (!existsSync(barrelPath)) {
2459
+ logError(`${dirName}/${entry}/ is missing required ${BARREL_FILE} barrel file`);
2460
+ } else {
2461
+ // Verify barrel exports at least one member
2462
+ const barrelContent = readdirSync(entryPath);
2463
+ const tsFiles = barrelContent.filter(f => isTsFile(f) && f !== BARREL_FILE);
2464
+ if (tsFiles.length === 0) {
2465
+ logWarning(`${dirName}/${entry}/${BARREL_FILE} exists but no implementation files found in this domain`);
2466
+ }
2467
+ }
2468
+
2469
+ // Check no nested subdirectories (domains should be flat)
2470
+ const subEntries = readdirSync(entryPath);
2471
+ for (const subEntry of subEntries) {
2472
+ const subPath = join(entryPath, subEntry);
2473
+ if (subEntry !== 'test' && subEntry !== '__tests__' && subEntry !== '__mocks__' && isDirectory(subPath)) {
2474
+ logWarning(`${dirName}/${entry}/ should not contain nested subdirectory "${subEntry}/" -- keep domains flat`);
2475
+ }
2476
+ }
2477
+ } else if (isTsFile(entry)) {
2478
+ // This is a flat file at the directory root
2479
+ if (!allowedRoot.includes(entry)) {
2480
+ logError(
2481
+ `Flat file "${dirName}/${entry}" is not allowed at the root level. ` +
2482
+ `Move it to a domain subdirectory (e.g., ${dirName}/${entry.replace(/\.(ts|tsx)$/, '')}/${entry}). ` +
2483
+ `Allowed root files: ${allowedRoot.join(', ') || 'none'}`
2484
+ );
2485
+ }
2486
+ }
2487
+ }
2488
+ }
2489
+
2490
+ // Also run the ESLint check (catches deep imports bypassing barrels)
2491
+ try {
2492
+ const { execSync } = await import('child_process');
2493
+ console.log('\n 🔍 Verifying no deep imports bypass barrels...');
2494
+ execSync('pnpm lint', { cwd: PROJECT_ROOT, stdio: 'inherit' });
2495
+ } catch {
2496
+ logError('ESLint found violations (may include deep imports bypassing barrel files)');
2497
+ }
2498
+
2499
+ console.log(`\n${errors === 0 ? '✅' : '❌'} Modularization check complete. Errors: ${errors}, Warnings: ${warnings}\n`);
2500
+
2501
+ if (errors > 0) {
2502
+ console.error('Fix the errors above and try again.\n');
2503
+ process.exit(1);
2504
+ }
2505
+
2506
+ if (warnings > 0) {
2507
+ console.warn('Warnings found. Consider addressing them.\n');
2508
+ }
2509
+ ```
2510
+
2511
+ **What this enforces at commit time**:
2512
+ - ESLint auto-fixes what it can, then fails hard on remaining errors (`--max-warnings 0`)
2513
+ - Prettier formats all staged files
2514
+ - Commitlint rejects non-conventional commit messages
2515
+ - Pre-push runs the full quality suite (lint, modularization, types, tests)
2516
+ - Modularization script validates folder structure AND barrel file compliance
2517
+
2518
+ ### 19.3 Commitlint Configuration
2519
+
2520
+ ```javascript
2521
+ // commitlint.config.js
2522
+ export default {
2523
+ extends: ['@commitlint/config-conventional'],
2524
+ rules: {
2525
+ 'type-enum': [
2526
+ 'error',
2527
+ [
2528
+ 'feat',
2529
+ 'fix',
2530
+ 'security',
2531
+ 'perf',
2532
+ 'docs',
2533
+ 'style',
2534
+ 'refactor',
2535
+ 'test',
2536
+ 'chore',
2537
+ ],
2538
+ ],
2539
+ 'subject-max-length': [2, 'always', 100],
2540
+ 'body-max-line-length': [2, 'always', 100],
2541
+ },
2542
+ };
2543
+ ```
2544
+
2545
+ ### 19.4 ESLint Rules That Must Be Enforced (with Error Severity)
2546
+
2547
+ These rules translate the standards in Section 5 into machine-enforceable lint rules:
2548
+
2549
+ ```javascript
2550
+ // packages/config/eslint/base.mjs (add these to the rules block)
2551
+ rules: {
2552
+ // === NAMING (Section 5.2) ===
2553
+ 'id-length': ['error', {
2554
+ min: 2,
2555
+ max: 50,
2556
+ exceptions: ['i', 'j', 'k', '_', 'x', 'y', 'z'],
2557
+ properties: 'never',
2558
+ }],
2559
+
2560
+ camelcase: ['error', {
2561
+ properties: 'never',
2562
+ ignoreDestructuring: false,
2563
+ allow: ['^UNSAFE_'],
2564
+ }],
2565
+
2566
+ 'unicorn/filename-case': ['error', {
2567
+ case: 'kebabCase',
2568
+ ignore: ['^[A-Z].*\\.tsx$'], // PascalCase for component files
2569
+ }],
2570
+
2571
+ 'react/jsx-pascal-case': ['error', {
2572
+ allowAllCaps: true, // Allow HOCs like withAuth()
2573
+ }],
2574
+
2575
+ // === IMPORTS (Section 5.3) ===
2576
+ 'import/order': ['error', {
2577
+ groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
2578
+ 'newlines-between': 'always',
2579
+ alphabetize: { order: 'asc', caseInsensitive: true },
2580
+ }],
2581
+
2582
+ // === FUNCTIONS (Section 5.2) ===
2583
+ 'prefer-arrow-callback': 'error',
2584
+ 'func-style': ['error', 'expression'],
2585
+ 'require-await': 'error',
2586
+
2587
+ // === NULL SAFETY ===
2588
+ 'unicorn/no-null': 'error',
2589
+
2590
+ // === REACT HOOKS (Section 18) ===
2591
+ 'react-hooks/rules-of-hooks': 'error',
2592
+ 'react-hooks/exhaustive-deps': 'warn',
2593
+
2594
+ // === LOVABLE GENERATED CODE (Section 5.4) ===
2595
+ 'no-unused-vars': ['error', {
2596
+ varsIgnorePattern: '^_',
2597
+ argsIgnorePattern: '^_',
2598
+ }],
2599
+
2600
+ '@typescript-eslint/no-explicit-any': 'error',
2601
+ '@typescript-eslint/no-unsafe-assignment': 'error',
2602
+ '@typescript-eslint/no-unsafe-call': 'error',
2603
+ '@typescript-eslint/no-unsafe-member-access': 'error',
2604
+
2605
+ // === PRETTIER ===
2606
+ 'prettier/prettier': ['error', {
2607
+ semi: true,
2608
+ singleQuote: true,
2609
+ tabWidth: 2,
2610
+ trailingComma: 'es5',
2611
+ printWidth: 100,
2612
+ }],
2613
+
2614
+ // === CONSOLE & DEBUGGER (Section 13.4) ===
2615
+ 'no-console': ['error', { allow: ['warn', 'error'] }],
2616
+ 'no-debugger': 'error',
2617
+
2618
+ // === MODULARIZATION & BARREL FILE ENFORCEMENT (Section 3.3.1, 5.2.1) ===
2619
+ 'boundaries/entry-point': ['error', {
2620
+ default: 'disallow',
2621
+ rules: [
2622
+ { target: ['src/services/**/*.ts'], allow: 'src/services/api-client.ts' },
2623
+ { target: ['src/hooks/use-employee*.ts', 'src/hooks/use-auth*.ts', 'src/hooks/use-user*.ts'], disallow: 'src/hooks/' },
2624
+ { target: ['src/store/**/*.ts'], allow: 'src/store/index.ts' },
2625
+ ],
2626
+ }],
2627
+
2628
+ 'boundaries/element-types': ['error', {
2629
+ default: 'disallow',
2630
+ rules: [
2631
+ { from: ['employee', 'auth', 'users'], allow: ['shared'] },
2632
+ { from: ['employee'], disallow: ['auth', 'users'] },
2633
+ { from: ['auth'], disallow: ['employee', 'users'] },
2634
+ { from: ['users'], disallow: ['employee', 'auth'] },
2635
+ { from: ['shared'], allow: ['shared'] },
2636
+ ],
2637
+ }],
2638
+
2639
+ // BLOCK: deep imports bypassing barrel files (imports must go through index.ts)
2640
+ 'import/no-internal-modules': ['error', {
2641
+ allow: [
2642
+ 'src/services/*/index',
2643
+ 'src/hooks/*/index',
2644
+ 'src/store/*/index',
2645
+ 'src/components/*/index',
2646
+ 'src/lib/*/index',
2647
+ 'src/types/*/index',
2648
+ 'src/services/api-client',
2649
+ 'src/hooks/use-debounce',
2650
+ 'src/hooks/use-media-query',
2651
+ 'src/components/ui/**',
2652
+ 'src/lib/*',
2653
+ 'src/types/*',
2654
+ 'src/app/**',
2655
+ 'src/styles/**',
2656
+ 'src/test/**',
2657
+ ],
2658
+ }],
2659
+ }
2660
+ ```
2661
+
2662
+ ### 19.5 CI Validation Scripts (package.json)
2663
+
2664
+ Create a `scripts/` directory with reusable validation:
2665
+
2666
+ ```json
2667
+ // package.json
2668
+ {
2669
+ "scripts": {
2670
+ "lint": "eslint --ext .ts,.tsx --max-warnings 0 .",
2671
+ "lint:fix": "eslint --ext .ts,.tsx . --fix",
2672
+ "format": "prettier --write \"src/**/*.{ts,tsx,json,css,md}\"",
2673
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,json,css,md}\"",
2674
+ "check-types": "tsc --noEmit",
2675
+ "prepare": "husky",
2676
+ "validate": "pnpm lint && pnpm check-types",
2677
+ "test:validate": "pnpm test:ci --coverage"
2678
+ }
2679
+ }
2680
+ ```
2681
+
2682
+ ### 19.6 CI Pipeline Enforcement (GitHub Actions)
2683
+
2684
+ Every CI run must check these in order. Any failure blocks the PR:
2685
+
2686
+ ```yaml
2687
+ # .github/workflows/ci.yml (excerpt)
2688
+ jobs:
2689
+ quality:
2690
+ runs-on: ubuntu-latest
2691
+ steps:
2692
+ - uses: actions/checkout@v4
2693
+ with:
2694
+ fetch-depth: 0
2695
+ - uses: actions/setup-node@v4
2696
+ - uses: pnpm/action-setup@v2
2697
+ - run: pnpm install --frozen-lockfile
2698
+
2699
+ # LAYER 3: Automated enforcement
2700
+ - name: Validate Commit Messages (anti-bypass for --no-verify)
2701
+ if: github.event_name == 'pull_request'
2702
+ run: pnpm exec commitlint --from origin/${{ github.base_ref }} --to HEAD
2703
+
2704
+ - name: Lint ( ESLint + naming rules + modularization )
2705
+ run: pnpm lint
2706
+ # Fails on: naming, imports, types, prettier, no-unused-vars, cross-domain imports
2707
+
2708
+ - name: Check Modularization ( domain structure + barrels )
2709
+ run: pnpm check-modularization
2710
+ # Fails on: flat files at directory roots, missing barrel files, deep imports
2711
+
2712
+ - name: Type Check ( TypeScript strict )
2713
+ run: pnpm check-types
2714
+ # Fails on: any TS type error, missing types, implicit any
2715
+
2716
+ - name: Format Check ( Prettier )
2717
+ run: pnpm format:check
2718
+ # Fails on: any formatting deviation
2719
+
2720
+ - name: Test with Coverage ( Vitest )
2721
+ run: pnpm test:validate
2722
+ # Fails on: test failures OR coverage < 80%
2723
+
2724
+ - name: Security Audit ( pnpm audit )
2725
+ run: pnpm audit --audit-level=high
2726
+ # Fails on: high/critical vulnerabilities
2727
+
2728
+ - name: Build
2729
+ run: pnpm build
2730
+ # Fails on: TS errors missed by tsc, bundler errors
2731
+ ```
2732
+
2733
+ ### 19.7 Coverage Threshold Enforcement
2734
+
2735
+ > **Note**: Vitest v3 deprecated the top-level `lines`/`branches`/`functions`/`statements` shorthand in the `coverage` block. Use `thresholds` exclusively.
2736
+
2737
+ ```javascript
2738
+ // vitest.config.ts
2739
+ import { defineConfig } from 'vitest/config';
2740
+
2741
+ export default defineConfig({
2742
+ test: {
2743
+ coverage: {
2744
+ provider: 'v8',
2745
+ reporter: ['text', 'json', 'html', 'cobertura'],
2746
+ thresholds: {
2747
+ lines: 80,
2748
+ branches: 75,
2749
+ functions: 80,
2750
+ statements: 80,
2751
+ // Each new file must have at least 90%
2752
+ perFile: true,
2753
+ },
2754
+ },
2755
+ },
2756
+ });
2757
+ ```
2758
+
2759
+ ### 19.8 Pre-commit Validation Script Template
2760
+
2761
+ ```javascript
2762
+ // scripts/validate-pre-commit.js
2763
+ import { execSync } from 'child_process';
2764
+ import chalk from 'chalk';
2765
+
2766
+ const checks = [
2767
+ { name: 'Lint', command: 'pnpm lint' },
2768
+ { name: 'Type Check', command: 'pnpm check-types' },
2769
+ ];
2770
+
2771
+ let failed = false;
2772
+
2773
+ for (const check of checks) {
2774
+ try {
2775
+ console.log(chalk.blue(`Running ${check.name}...`));
2776
+ execSync(check.command, { stdio: 'inherit' });
2777
+ console.log(chalk.green(`${check.name} passed`));
2778
+ } catch (err) {
2779
+ console.error(chalk.red(`${check.name} FAILED`));
2780
+ failed = true;
2781
+ }
2782
+ }
2783
+
2784
+ if (failed) {
2785
+ console.error(chalk.red('\nPre-commit validation failed. Fix the above errors and try again.'));
2786
+ process.exit(1);
2787
+ }
2788
+
2789
+ console.log(chalk.green('\nAll pre-commit checks passed!'));
2790
+ ```
2791
+
2792
+ ### 19.9 What Each Layer Catches
2793
+
2794
+ | Layer | Catches | Cannot Catch |
2795
+ |-------|---------|--------------|
2796
+ | L1 Editor | Typos, bad names, bad formatting | Logic bugs, architecture flaws |
2797
+ | L2 Pre-commit | Same as L1 + blocks commit | Business logic errors |
2798
+ | L3 CI Pipeline | Everything automated can check | Business logic correctness |
2799
+ | L4 Code Review | Architecture, security, edge cases | Naming that already passed ESLint |
2800
+
2801
+ ### 19.10 Enforcement Checklist for New Projects
2802
+
2803
+ When setting up a new project, verify:
2804
+
2805
+ - [ ] Husky installed: `pnpm prepare` creates `.husky/`
2806
+ - [ ] Pre-commit hook exists and runs: `git commit --dry-run`
2807
+ - [ ] Commit-msg hook exists: Bad message is rejected
2808
+ - [ ] Pre-push hook exists: Tests run on `git push`
2809
+ - [ ] lint-staged config in package.json (not in separate config file)
2810
+ - [ ] `pnpm lint` exits non-zero on any ESLint error (including boundaries + `no-internal-modules`)
2811
+ - [ ] `pnpm check-modularization` exits non-zero on folder structure / barrel violations
2812
+ - [ ] `pnpm check-types` exits non-zero on any TS error
2813
+ - [ ] `pnpm format:check` exits non-zero on unformatted files
2814
+ - [ ] `pnpm test:ci` enforces coverage thresholds (80% lines, 75% branches)
2815
+ - [ ] `pnpm audit --audit-level=high` runs in CI
2816
+ - [ ] GitHub Actions CI blocked on lint/modularization/type/test failures (not `continue-on-error`)
2817
+ - [ ] Branch protection requires CI to pass before merge
2818
+ - [ ] Every domain subdirectory has an `index.ts` barrel file
2819
+ - [ ] No flat domain files at `services/`, `hooks/`, `store/`, `components/`, `lib/` roots
2820
+ - [ ] `.github/PULL_REQUEST_TEMPLATE.md` exists (copied from `@aoholdings/frontend-standards`)
2821
+ - [ ] All PRs follow the template format
2822
+ - [ ] Reviewer knows the standards in this document
2823
+
2824
+ ---
2825
+
2826
+ > **This is a living document.** It should be reviewed and updated quarterly by the frontend chapter lead and architecture team. Any deviation from these standards must be documented in an Architecture Decision Record (ADR).
2827
+
2828
+ ---
2829
+ ## Appendix C. Frontend Cost & Pricing Reference (June 2026)
2830
+
2831
+ > Use this to estimate implementation and operational costs when proposing a new project or staffing decisions. Prices are approximations for mid-market commercial engagements and can vary by vendor, region, and contract length.
2832
+
2833
+ ### Partner Benefits & Azure Cost Relief
2834
+
2835
+ As a **Microsoft Partner**, your organization may qualify for:
2836
+
2837
+ - **Microsoft Action Pack / Solutions Partner** benefits: included Azure credits, free/dev CI-CD minutes, and co-selling/support access.
2838
+ - **Azure Hybrid Benefit / Reserved Instances**: reduces VM and managed-service compute cost by **20%–40%**.
2839
+ - **Visual Studio Enterprise / GitHub Enterprise** bundled licensing: avoids duplicate license spend and typically lowers total cost of ownership.
2840
+ - **Azure for Startups / ISV Pied Piper**: early-stage credits and advisory hours to reduce ramp-up cost.
2841
+ - **FastTrack / Partner Support**: faster migrations and production-readiness reviews reduce delivery risk and project overruns.
2842
+
2843
+ Use this table to compare partner-inclusive pricing vs. standard list pricing when planning workloads.
2844
+
2845
+ ### C.1 Estimated Annual Costs by Project Size
2846
+
2847
+ | Project Size | Frontend Devs | Backend Devs | DevOps | QA | Tools & Licenses | Infrastructure | Total Est. Annual |
2848
+ |---|---|---|---|---|---|---|---|
2849
+ | Small (MVP/closed beta) | 1 | 1 | 0.25 | 0.5 | ~USD 2,000 | ~USD 1,500 | ~USD 160,000 |
2850
+ | Medium (production SaaS) | 2 | 2 | 0.5 | 1 | ~USD 5,000 | ~USD 3,500 | ~USD 350,000 |
2851
+ | Large (enterprise/multi-tenant) | 4 | 4 | 1 | 2 | ~USD 12,000 | ~USD 8,000 | ~USD 800,000 |
2852
+ | Enterprise (regulated/high-scale) | 6+ | 6+ | 2+ | 3+ | ~USD 25,000+ | ~USD 20,000+ | ~USD 1,600,000+ |
2853
+
2854
+ ### C.2 Tooling & License Costs (Annual)
2855
+
2856
+ | Tool / Service | Purpose | Typical Tier | Est. Cost | Notes |
2857
+ |---|---|---|---|---|
2858
+ | GitHub Enterprise / Teams | Repo hosting, Actions minutes, GHCR, Environments | Team: USD 4/user/mo; Enterprise: USD 21/user/mo | USD 2,400 – USD 25,000/yr | Public repos have free Actions minutes; private minutes are billed beyond the free tier (2,000 min/mo). |
2859
+ | Sentry (Error Tracking) | Frontend & backend error monitoring | Team: ~USD 29/app/mo | USD 350+/app/yr | Volume-based; consider self-hosted at scale. |
2860
+ | Datadog / New Relic / Azure Monitor | APM, logs, traces | Host-based or ingestion-based | USD 1,500 – USD 10,000+/yr | Strong for microservices; may overlap with Prometheus/Grafana. |
2861
+ | SonarCloud / SonarQube | Static analysis, quality gate | Developer: ~USD 25/dev/mo; Enterprise billed annually | USD 2,000 – USD 15,000/yr | SonarCloud is SaaS; SonarQube can be self-hosted with infra cost. |
2862
+ | Snyk / Trivy / Dependabot | Dependency & container vulnerability scanning | Snyk Team: ~USD 19/dev/mo; Trivy is free (OSS) | USD 0 – USD 5,000/yr | Trivy + GitHub Dependabot cover most needs at no extra cost. |
2863
+ | Vercel / Netlify / Azure Static Web Apps | Frontend preview deploys and hosting | Pro: ~USD 20/user/mo | USD 2,400 – USD 8,000/yr | GHCR + K8s can replace this; preview environments add cost. |
2864
+ | Azure Container Registry (ACR) or AWS ECR | Container image registry | Basic: USD 0.167/day + storage/egress | USD 100 – USD 1,000/yr | Often cheaper than GHCR at high throughput; pay for storage + egress. |
2865
+ | Auth0 / WorkOS / Clerk | Managed auth (if not custom) | Free to USD 23+/mo | USD 0 – USD 2,000/yr | Custom NestJS auth saves license cost but needs more engineering time. |
2866
+ | Storybook | Component library documentation + review | OSS free; Cloud from USD 20/mo | USD 0 – USD 5,000/yr | Includes collaboration add-ons in paid tiers. |
2867
+
2868
+ ### C.3 Infrastructure Costs (Monthly)
2869
+
2870
+ | Environment | Typical Specs | Est. Monthly | Annual Est. |
2871
+ |---|---|---|---|
2872
+ | Dev (1 replica, no HA) | 1 vCPU / 2 GB RAM | USD 40 – USD 120 | USD 500 – USD 1,500 |
2873
+ | Staging (2 replicas) | 2 vCPU / 4 GB RAM each | USD 120 – USD 300 | USD 1,500 – USD 3,600 |
2874
+ | Production (min 3 replicas, autoscale) | 2 vCPU / 4 GB RAM each + load balancer | USD 300 – USD 900 | USD 3,600 – USD 10,800 |
2875
+ | Managed PostgreSQL (e.g., Azure Database, RDS, Cloud SQL) | 2 vCPU / 8 GB + storage | USD 150 – USD 500 | USD 1,800 – USD 6,000 |
2876
+ | Managed Redis / Valkey | 1 vCPU / 2 GB | USD 15 – USD 50 | USD 180 – USD 600 |
2877
+ | Object Storage (S3 / Blob) | 1 TB + egress | USD 20 – USD 80 | USD 240 – USD 960 |
2878
+ | SSL (managed by ingress or ACM) | -- | Often free | USD 0 |
2879
+ | DNS / domain | -- | USD 10 – USD 30 | USD 120 – USD 360 |
2880
+
2881
+ > These are mid-2026 market reference prices. Cloud providers frequently discount reserved/commitment terms.
2882
+
2883
+ ### C.4 CDN & Hosting Notes
2884
+
2885
+ - For multi-region low-latency asset delivery, expect an additional USD 10 – USD 80/month depending on cache hit ratio.
2886
+ - Pre-rendering, caching strategies, and service workers reduce bandwidth and infrastructure cost more than most premature optimizations.
2887
+
2888
+ ---
2889
+
2890
+ ### C.5 Open-Source vs Commercial Alternatives (2026)
2891
+
2892
+ | Need | Commercial Option | OSS Alternative | Est. Savings | Trade-offs / Notes |
2893
+ |---|---|---|---|---|
2894
+ | CI/CD | GitHub Actions (private) | **Woodpecker**, **Gitea Actions**, Jenkins, GitLab CI (self-hosted runners) | Up to ~USD 10,000/yr on high volume | Self-hosted runners need infra + maintenance. Woodpecker/Gitea are easiest to adopt if you want to stay off cloud minutes. |
2895
+ | Error Tracking | Sentry Team | **Sentry Self-Hosted**, GlitchTip | ~USD 350 – USD 3,000+/yr saved | Self-hosted Sentry needs ~1–2 vCPU + 2–4 GB RAM per app. GlitchTip is lighter. |
2896
+ | Monitoring/APM | Datadog / New Relic | **Prometheus + Grafana**, SigNoz, Uptime Kuma | ~USD 1,500 – USD 10,000+/yr saved | OSS has higher setup cost. Best for teams with in-house SRE knowledge. |
2897
+ | Static Analysis | SonarCloud | **SonarQube Community**, ESLint + TypeScript strict + Coverage, CodeQL | ~USD 2,000 – USD 15,000/yr saved | SonarQube CE is capable. GitHub CodeQL is free for public and private repos. |
2898
+ | Dependency Scanning | Snyk | **Trivy**, OWASP Dependency-Check, GitHub Dependabot, Renovate | ~USD 0 – USD 5,000/yr saved | Dependabot + Trivy cover the majority of needs at zero license cost. |
2899
+ | Auth (if not custom) | Auth0 / Clerk | **Keycloak**, Authentik, SuperTokens | ~USD 0 – USD 2,000/yr saved | Keycloak is the most mature but heavier. SuperTokens is lighter and developer-friendly. |
2900
+ | Component Review | Storybook Cloud | **Storybook OSS**, Chromatic (paid) | ~USD 0 – USD 5,000/yr saved | Storybook OSS is fully free. Chromatic adds hosted review at ~USD 20/app/mo. |
2901
+ | Frontend Hosting | Vercel / Netlify / SWA | **Nginx + Cloudflare**, Caddy, self-hosted K8s + GHCR | ~USD 2,400 – USD 8,000/yr saved | GHCR + K8s is usually cheaper at scale; preview environments cost more effort. |
2902
+ | Container Registry | ACR / ECR | **GHCR**, Harbor (self-hosted) | ~USD 100 – USD 1,000/yr saved | GHCR is free for public/private with GitHub Actions. Harbor is good for air-gapped/on-prem. |
2903
+
2904
+ ### C.6 OSS Cost-Scoring Cheat Sheet
2905
+
2906
+ | Solution | Setup Effort | Maintenance | When It Wins | When to Buy Commercial |
2907
+ |---|---|---|---|---|
2908
+ | Prometheus + Grafana | Medium | Medium | Metrics-heavy infra, in-house SRE skill | When you want hosted dashboards, alerting, and support SLAs |
2909
+ | Sentry Self-Hosted | Medium | Medium | Apps with many errors and sensitive logs | When uptime and incident response are more important than license cost |
2910
+ | Keycloak | High | High | Enterprise SSO, internal identity, B2B | When identity is not your core competency |
2911
+ | Woodpecker / Gitea | Medium | Medium | Heavy CI usage, private repo cost control | When you want zero per-minute billing and full data control |
2912
+ | Jenkins | High | High | Legacy enterprise estates, Windows workloads | When your team already knows Jenkins and plugin ecosystem |
2913
+ | Harbor | Medium | Medium | Air-gapped/on-prem registries, large images | When egress or compliance prevents cloud registries |
2914
+
2915
+ ### C.7 Backend-Oriented Cost Notes (Frontend Impact)
2916
+
2917
+ - **Serverless / Edge**: If the backend is serverless (Lambda/Cloud Functions), frontend should aim for fewer, batched API calls to reduce invocation costs.
2918
+ - **Caching**: Client-side caching (React Query, SW) and CDN caching reduce backend and bandwidth spend.
2919
+ - **Monitoring**: OSS frontend monitoring stack (Sentry self-hosted + Grafana) can replace Datadog RUM for ~USD 1,500 – USD 3,000/yr with some ops effort.
2920
+ - **Rate Limiting**: Implemented correctly in the frontend (debounce/cancel stale requests), it reduces backend compute and queuing costs.
2921
+
2922
+ ### C.8 Sources & Methodology
2923
+
2924
+ - Pricing is based on **public list prices** from vendor pricing pages:
2925
+ - Azure pricing: https://azure.microsoft.com/en-us/pricing/
2926
+ - Azure Static Web Apps: https://azure.microsoft.com/en-us/pricing/details/app-service/static/
2927
+ - GitHub: https://github.com/pricing
2928
+ - Sentry: https://sentry.io/pricing/
2929
+ - Datadog: https://www.datadoghq.com/pricing/
2930
+ - SonarCloud: https://www.sonarsource.com/pricing/
2931
+ - Snyk: https://snyk.io/pricing/
2932
+ - Vercel: https://vercel.com/pricing
2933
+ - Netlify: https://www.netlify.com/pricing/
2934
+ - Storybook: https://storybook.js.org/docs/configure/overview#tooling-options
2935
+ - Microsoft Partner benefits sourced from:
2936
+ - Microsoft Partner Network: https://partner.microsoft.com/
2937
+ - Microsoft for Startups / ISV Pied Piper: https://startups.microsoft.com/
2938
+ - Azure Hybrid Benefit: https://azure.microsoft.com/en-us/pricing/benefits/azure-hybrid-benefit/
2939
+ - Visual Studio subscriptions: https://visualstudio.microsoft.com/subscriptions/
2940
+ - FastTrack: https://azure.microsoft.com/en-us/programs/azure-fasttrack/
2941
+ Actual entitlement depends on partner tier, agreement, and region; treat these as **items to explore** with your account team.
2942
+ - Cloud infrastructure ranges are derived from standard managed VM, DB, and storage pricing pages; costs can be lower with reserved instances, savings plans, or partner credits.