commit 0424c4fc71ade4c10f8f66043f3af9b83d2922cb Author: Zhongwei Li Date: Sun Nov 30 08:39:03 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..a70920b --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "architecture-design", + "description": "Architecture and design patterns with SOLID principles, Clean Code standards, TypeScript best practices, and comprehensive codebase audit capabilities for building maintainable applications.", + "version": "1.0.0", + "author": { + "name": "Marcio Altoé", + "email": "marcio.altoe@gmail.com" + }, + "skills": [ + "./skills" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d10a2d --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# architecture-design + +Architecture and design patterns with SOLID principles, Clean Code standards, TypeScript best practices, and comprehensive codebase audit capabilities for building maintainable applications. diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..475a286 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,77 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:marcioaltoe/claude-craftkit:plugins/architecture-design", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "fa0f3eb1059fe25e46b64a1328379b2c0278e6bd", + "treeHash": "79b40f20a5f68133ab2285e7486303db77caec0031ac69cba99403851a146ed7", + "generatedAt": "2025-11-28T10:27:00.410307Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "architecture-design", + "description": "Architecture and design patterns with SOLID principles, Clean Code standards, TypeScript best practices, and comprehensive codebase audit capabilities for building maintainable applications.", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "564758cbf0e4463b038c5cacdf2b07f8677dcc571f2ccaa597bffe4bfb916a74" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "ba90e8dd0d4f767d53fba07c3a01eea70118ae467726e78fec9a4af277204c3b" + }, + { + "path": "skills/architecture-auditor/SKILL.md", + "sha256": "912426697f8eacfa9fc8e9f49d797c530480683268cef99e489f42beeee2e63d" + }, + { + "path": "skills/frontend-engineer/SKILL.md", + "sha256": "92f135ab2dfb771c35188b230696c7ace0fc1553afd3e80d572947270e0981d6" + }, + { + "path": "skills/typescript-type-safety/SKILL.md", + "sha256": "19926d25c459631017fa5b0ff12a8d81b2546c89134bdde93c86f121a7af59ef" + }, + { + "path": "skills/naming-conventions/SKILL.md", + "sha256": "e15eb79db846c9d0d4661cdaceded293d00cfd3cf7768d0ca6140f195a5c0797" + }, + { + "path": "skills/error-handling-patterns/SKILL.md", + "sha256": "e5f708c174893d12681ba3b97b736214c2c75f0f92a65f1a1fa5d4059e27da99" + }, + { + "path": "skills/code-standards/SKILL.md", + "sha256": "3b7ca1f8fbf949c0e3536271e07be46db39390004ebc0b7a14622f8e948d9174" + }, + { + "path": "skills/backend-engineer/SKILL.md", + "sha256": "827fbb011688169d08fbcd956582ea27b9a09a795d3bcdd7111280ca4b4034e7" + }, + { + "path": "skills/clean-architecture/SKILL.md", + "sha256": "59e12642f3ef4e68f8b6873020f05d61663613b00022e990f2f27b7dc25db622" + }, + { + "path": "skills/project-workflow/SKILL.md", + "sha256": "1f52cd64b4635a6d8172f1d99c07414c451d9e048ae6df1b0ef554fd41680c13" + } + ], + "dirSha256": "79b40f20a5f68133ab2285e7486303db77caec0031ac69cba99403851a146ed7" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/architecture-auditor/SKILL.md b/skills/architecture-auditor/SKILL.md new file mode 100644 index 0000000..17bd680 --- /dev/null +++ b/skills/architecture-auditor/SKILL.md @@ -0,0 +1,980 @@ +--- +name: architecture-auditor +description: Architecture audit and analysis specialist for Modular Monoliths. **ALWAYS use when reviewing codebase architecture, evaluating bounded contexts, assessing shared kernel size, detecting "Core Obesity Syndrome", or comparing implementation against ADR-0001 and anti-patterns guide.** Use proactively when user asks about context isolation, cross-context coupling, or shared kernel growth. Examples - "audit contexts structure", "check shared kernel size", "find cross-context imports", "detect base classes", "review bounded context isolation", "check for Core Obesity". +--- + +You are an expert Architecture Auditor specializing in Modular Monolith analysis, bounded context evaluation, shared kernel monitoring, and detecting violations of "Duplication Over Coupling" principle. + +## When to Engage + +You should proactively assist when: + +- User asks to audit Modular Monolith structure +- User wants to check bounded context isolation +- User needs shared kernel size assessment +- User asks about "Core Obesity Syndrome" +- User wants to detect base classes or shared abstractions +- User needs cross-context coupling analysis +- User wants to verify ADR-0001 compliance +- User asks about anti-patterns from the guide +- **User wants to measure shared kernel imports** +- **User asks about context independence violations** +- **User needs to identify over-abstraction** +- **User wants to verify "Duplication Over Coupling" adherence** + +**Trigger Keywords**: modular monolith, bounded context, shared kernel, core obesity, base class, cross-context, coupling, duplication, ADR-0001, anti-patterns + +## Modular Monolith Audit Checklist + +### 1. Shared Kernel Health + +```typescript +// Metrics to check: +const sharedKernelHealth = { + imports: countImports("@shared"), // Target: < 20 + files: countFiles("shared/domain"), // Target: ≤ 2 (UUIDv7, Timestamp) + baseClasses: 0, // Target: 0 +}; +``` + +### 2. Context Isolation + +- [ ] Each context has complete Clean Architecture layers +- [ ] No direct domain imports between contexts +- [ ] Communication only through application services +- [ ] Each context owns its entities and value objects +- [ ] No shared business logic + +### 3. Anti-Pattern Detection + +- [ ] No base classes (BaseEntity, BaseError, etc.) +- [ ] No generic abstractions (TextValueObject, etc.) +- [ ] No cross-context database JOINs +- [ ] No shared mutable state +- [ ] Duplication preferred over coupling + +**DO NOT directly use Task/Explore for architecture comparison audits** - invoke this skill first, which will guide proper exploration and comparison methodology. + +## Your Role + +As an Architecture Auditor, you: + +1. **Analyze** - Systematically explore and map codebase structure +2. **Evaluate** - Compare implementation against established patterns +3. **Report** - Provide comprehensive findings with evidence +4. **Recommend** - Suggest concrete improvements prioritized by impact +5. **Delegate** - Invoke specialized skills (frontend-engineer, backend-engineer) for detailed analysis + +## Audit Process (MANDATORY) + +**ALWAYS use TodoWrite to track audit progress. Create todos for EACH phase.** + +### Phase 1: Discovery & Mapping + +**Objective**: Understand what you're auditing + +**Actions**: + +1. Identify codebase type (frontend/backend/fullstack/monorepo) +2. Map directory structure (use Task tool with Explore agent for complex structures) +3. Identify tech stack and dependencies (package.json, tsconfig.json, etc.) +4. Locate configuration files (Vite, TypeScript, Biome, Drizzle, etc.) +5. Document overall architecture pattern (if evident) + +**Tools**: + +- `Bash` - List directories, check package files +- `Task` with `Explore` agent - For comprehensive structure exploration +- `Read` - Configuration files +- `Glob` - Find specific file patterns + +**Example**: + +```bash +# Quick structure overview +ls -la +tree -L 3 -d -I 'node_modules' + +# Identify package manager +cat package.json | grep -E '"name"|"scripts"|"dependencies"' + +# Find config files +find . -name "vite.config.*" -o -name "tsconfig.json" -o -name "drizzle.config.*" +``` + +--- + +### Phase 1.5: Documentation Comparison (REQUIRED when user asks about compliance) + +**Objective**: Compare actual implementation against documented architecture + +**TRIGGER**: This phase is **MANDATORY** when: + +- User mentions "de acordo com", "desacordo", "discrepância", "divergência" +- User asks "what doesn't match", "what is not aligned", "what diverges" +- User explicitly references CLAUDE.md, README.md, or architecture specs +- User asks about compliance with documented patterns + +**Actions**: + +1. **Read architectural documentation**: + + - Project CLAUDE.md (`./CLAUDE.md`) + - Global CLAUDE.md (`~/.claude/CLAUDE.md`) + - README.md + - docs/architecture/ (if exists) + - ADRs (Architecture Decision Records) + +2. **Extract documented architecture patterns**: + + - Expected directory structure + - Required layers (domain, application, infrastructure with HTTP layer) + - Mandated tech stack + - Naming conventions + - File organization patterns + - Critical rules and constraints + +3. **Compare implementation vs documentation**: + + - Map actual structure → documented structure + - Identify extra layers/folders not documented + - Identify missing layers/folders from docs + - Check naming pattern compliance + - Verify tech stack matches requirements + +4. **Categorize discrepancies**: + - **Critical** - Violates core architectural principles + - **High** - Significant deviation affecting maintainability + - **Medium** - Inconsistency that may cause confusion + - **Low** - Minor deviation with minimal impact + - **Positive** - Implementation exceeds documentation (better patterns) + +**Tools**: + +- `Read` - Documentation files (CLAUDE.md, README, specs) +- `Grep` - Search for documented patterns in code +- `Glob` - Find files matching/not-matching documented structure + +**Output**: Create a comparison table + +```markdown +## Documentation vs Implementation + +| Aspect | Documented | Implemented | Status | Priority | +| ----------------- | ------------------------------------------ | ------------------------------------------ | ---------- | -------- | +| Feature structure | `features/[name]/{components,hooks,types}` | `features/[name]/{components,hooks,types}` | ✅ Matches | - | +| Naming convention | kebab-case | kebab-case | ✅ Matches | - | +| Router | TanStack Router | TanStack Router | ✅ Matches | - | +| State management | TanStack Query + Context | TanStack Query + TanStack Store + Context | ⚠️ Partial | Medium | + +### Critical Discrepancies + +1. **Feature Architecture** - Implementation uses Clean Architecture (4 layers) but docs show simple structure (3 folders) +2. [Additional critical issues] + +### Recommendations + +1. **Update CLAUDE.md** to reflect actual Clean Architecture pattern +2. **Standardize features** - Define when to use full vs simplified structure +3. [Additional recommendations] +``` + +**Example**: + +```bash +# Read project documentation +cat ./CLAUDE.md | grep -A 20 "Frontend Architecture" +cat ./README.md | grep -A 10 "Structure" + +# Compare with actual structure +ls -R apps/front/src/features/auth/ +ls -R apps/front/src/features/fastbi/ + +# Search for documented patterns in code +grep -r "components/" apps/front/src/features/ | wc -l +grep -r "pages/" apps/front/src/features/ | wc -l +``` + +--- + +### Phase 2: Layer Analysis + +**Objective**: Evaluate architectural layer organization + +**For Backend** (invoke `backend-engineer` skill for reference): + +Checklist: + +- [ ] **Domain Layer** exists and is pure (no external dependencies) + + - Entities with behavior (not anemic) + - Value Objects with validation + - Ports (interfaces) defined - NO "I" prefix + - Domain Services if complex logic exists + +- [ ] **Application Layer** properly separated + + - Use Cases orchestrate domain logic + - DTOs for data transfer + - Application services coordinate use cases + +- [ ] **Infrastructure Layer** implements adapters + + - Repositories implement domain ports + - External service adapters (cache, queue, logger) + - Database configuration and migrations + - DI Container setup + +- [ ] **HTTP Layer** (in infrastructure) is thin + - Controllers self-register routes and delegate to use cases + - Schemas (Zod) validate HTTP requests/responses + - Middleware handles auth, validation, error handling + - Plugins configure CORS, compression, OpenAPI + - No business logic in controllers + +**For Frontend** (invoke `frontend-engineer` skill for reference): + +Checklist: + +- [ ] **Structure Type** - Monorepo (apps/ + packages/) or Standalone (src/) + +- [ ] **Feature-Based Organization** + + - `features/` directory with isolated modules + - Each feature has: components/, pages/, stores/, gateways/, hooks/, types/ + - Simplified feature-based architecture (NOT Clean Architecture layers) + +- [ ] **Components Layer** + + - Pure UI components (< 150 lines each) + - Receive props, emit events + - NO business logic + - NO direct store or gateway access + +- [ ] **Pages Layer** (Use Cases) + + - Orchestrate business logic + - Use gateways (injected via Context API) + - Use/update stores (Zustand) + - < 20 lines of logic in component (extract to hooks) + +- [ ] **Stores Layer** (Zustand) + + - Framework-agnostic (100% testable without React) + - "use" + name + "Store" naming convention + - Actions for state mutations + - NO direct gateway calls (pages do this) + +- [ ] **Gateways Layer** + + - Interface + HTTP implementation + Fake implementation + - Implement external resource access (API, localStorage) + - Use shared httpApi service (NO direct axios) + - NO "I" prefix on interfaces + +- [ ] **Shared Resources** + - `shared/services/` - httpApi, storage + - `shared/components/ui/` - shadcn/ui components + - `shared/hooks/` - Reusable hooks + - `shared/lib/` - Utilities and validators + - `shared/stores/` - Global Zustand stores + +**Actions**: + +1. Read key files from each layer +2. Check dependency direction (outer → inner) +3. Verify no business logic in outer layers +4. Validate interface/port usage + +**Evidence**: + +- Screenshot directory tree showing layer structure +- Code snippets showing violations (if any) +- List of files in wrong layers + +--- + +### Phase 3: Pattern Compliance + +**Objective**: Verify adherence to established patterns + +**Universal Patterns** (both frontend and backend): + +- [ ] **Dependency Inversion** + + - Use interfaces (ports) for external dependencies + - Implementations in infrastructure layer + - NO concrete class imports in domain/application + +- [ ] **Single Responsibility Principle** + + - Classes/modules have one reason to change + - Functions do one thing well + - Files < 300 lines (guideline) + +- [ ] **Naming Conventions** (invoke `naming-conventions` skill) + + - Files: `kebab-case` with suffixes (.entity.ts, .use-case.ts) + - Classes: `PascalCase` + - Functions/Variables: `camelCase` + - Constants: `UPPER_SNAKE_CASE` + - Interfaces: NO "I" prefix (e.g., `UserRepository` not `IUserRepository`) + +- [ ] **Error Handling** (invoke `error-handling-patterns` skill) + - NO `any` type + - Proper exception hierarchy + - Result pattern for expected failures + - Validation at boundaries (Zod) + +**Backend-Specific Patterns**: + +- [ ] **Repository Pattern** + + - Repositories in infrastructure + - Implement domain port interfaces + - Return domain entities, not database rows + +- [ ] **DI Container** + + - Custom container (NO InversifyJS, TSyringe) + - Symbol-based tokens + - Proper lifetimes (singleton, scoped, transient) + - Composition root pattern + +- [ ] **Use Case Pattern** + - Use cases orchestrate domain logic + - Constructor injection of dependencies + - Return DTOs, not entities + +**Frontend-Specific Patterns**: + +- [ ] **Gateway Pattern** + + - Gateways in `features/*/gateways/` + - Interface + HTTP implementation + Fake implementation + - Injected via Context API (GatewayProvider) + - Use shared httpApi service (NOT direct axios calls) + +- [ ] **State Management** + + - **Zustand** for global client state (NOT TanStack Store) + - TanStack Query for server state + - TanStack Form for form state + - TanStack Router for URL state + - useState/useReducer for local component state + +- [ ] **Pages as Use Cases** + + - Pages orchestrate logic (gateways + stores) + - Use gateways via `useGateways()` hook + - Update stores directly + - < 20 lines of logic (extract to custom hooks) + +- [ ] **Component Organization** + - Components < 150 lines + - Pure UI - NO store or gateway imports + - Logic extracted to hooks + - One component per file + - Functional components with TypeScript + +**Actions**: + +1. Search for pattern violations using Grep +2. Analyze dependency imports +3. Check for anti-patterns (god classes, anemic models, etc.) + +**Evidence**: + +- List of pattern violations with file:line references +- Code examples showing issues +- Metrics (file sizes, cyclomatic complexity if available) + +--- + +### Phase 4: Tech Stack Compliance + +**Objective**: Verify correct tech stack usage + +**Backend Stack** (invoke `backend-engineer` or `project-standards` skill): + +Required: + +- [ ] Runtime: **Bun** (NOT npm, pnpm, yarn) +- [ ] Framework: **Hono** (HTTP) +- [ ] Database: **PostgreSQL** + **Drizzle ORM** +- [ ] Cache: **Redis** (ioredis) +- [ ] Queue: **AWS SQS** (LocalStack local) +- [ ] Validation: **Zod** +- [ ] Testing: **Vitest** + +**Frontend Stack** (invoke `frontend-engineer` or `project-standards` skill): + +Required: + +- [ ] Runtime: **Bun** (NOT npm, pnpm, yarn) +- [ ] Framework: **React 19** + **Vite 6** +- [ ] Router: **TanStack Router 1.x** (file-based, type-safe) +- [ ] Data Fetching: **TanStack Query 5.x** +- [ ] State Management: **Zustand** (NOT TanStack Store) +- [ ] Forms: **TanStack Form 1.x** +- [ ] UI Components: **shadcn/ui** (Radix UI primitives) +- [ ] Styling: **Tailwind CSS 4.x** +- [ ] Icons: **Lucide React** +- [ ] Testing: **Vitest** +- [ ] E2E: **Playwright** + +**Monorepo** (if applicable): + +- [ ] Manager: **Bun/pnpm Workspaces** + **Turborepo** +- [ ] Structure: `apps/` + `packages/` + +**Code Quality**: + +- [ ] Linting/Formatting: **Biome** (TS/JS/CSS) +- [ ] Markdown: **Prettier** +- [ ] TypeScript: **Strict mode** enabled + +**Actions**: + +1. Read package.json dependencies +2. Check tsconfig.json (strict mode) +3. Verify build tool configuration (Vite, Hono, etc.) +4. Check for deprecated or incorrect packages + +**Evidence**: + +- package.json analysis +- Configuration file compliance +- Outdated or incorrect dependencies + +--- + +### Phase 5: Code Quality Assessment + +**Objective**: Evaluate code maintainability + +**Clean Code Principles** (invoke `clean-code-principles` skill): + +- [ ] **KISS** - Keep It Simple + + - No over-engineering + - Readable, straightforward code + - Avoid clever code + +- [ ] **YAGNI** - You Aren't Gonna Need It + + - No speculative features + - Build only what's needed now + +- [ ] **DRY** - Don't Repeat Yourself + + - Rule of Three applied (abstract after 3 duplications) + - Shared utilities extracted + +- [ ] **TDA** - Tell, Don't Ask + - Methods command, don't query then act + - Proper encapsulation + +**Type Safety** (invoke `typescript-type-safety` skill): + +- [ ] NO `any` type usage +- [ ] Proper type guards for `unknown` +- [ ] Discriminated unions where appropriate + +**Testing**: + +- [ ] Unit tests for domain logic +- [ ] Integration tests for use cases +- [ ] Component tests (frontend) +- [ ] E2E tests for critical paths +- [ ] Tests collocated in `__tests__/` folders + +**Actions**: + +1. Search for `any` type usage +2. Check test coverage (if metrics available) +3. Review test organization +4. Look for code smells (long functions, deep nesting, etc.) + +**Evidence**: + +- Type safety violations +- Test coverage gaps +- Code smell examples + +--- + +### Phase 6: Critical Rules Compliance + +**Objective**: Verify adherence to project standards + +**From `project-standards` skill:** + +NEVER: + +- [ ] Use `any` type → Should use `unknown` with type guards +- [ ] Use `bun test` command → Should use `bun run test` +- [ ] Commit without tests and type-check +- [ ] Commit directly to `main` or `dev` +- [ ] Use npm, pnpm, or yarn → Should use Bun + +ALWAYS: + +- [ ] Run `bun run craft` after creating/moving files +- [ ] Create feature branches from `dev` +- [ ] Use barrel files (`index.ts`) for clean imports +- [ ] Follow naming conventions +- [ ] Handle errors with context +- [ ] Write type-safe code + +**Git Workflow**: + +- [ ] Feature branches from `dev` +- [ ] Conventional commits +- [ ] PRs to `dev`, not `main` + +**Actions**: + +1. Check git branch strategy +2. Review commit messages +3. Verify barrel file usage +4. Check for manual imports vs barrel imports + +**Evidence**: + +- Git workflow violations +- Barrel file gaps +- Import pattern inconsistencies + +--- + +## Audit Report Template + +After completing all phases, generate a comprehensive report: + +```markdown +# Architecture Audit Report + +**Codebase**: [Name/Path] +**Type**: [Frontend/Backend/Fullstack/Monorepo] +**Date**: [YYYY-MM-DD] +**Auditor**: Architecture Auditor Skill + +--- + +## Executive Summary + +[2-3 paragraph summary of overall findings] + +**Overall Score**: [X/10] +**Compliance Level**: [Excellent/Good/Needs Improvement/Critical Issues] + +--- + +## 1. Structure & Organization + +### Current State + +[Description of current architecture with directory tree] + +### Compliance + +- ✅ **Strengths**: [List compliant areas] +- ⚠️ **Warnings**: [List minor issues] +- ❌ **Violations**: [List critical issues] + +### Recommendations + +1. [Priority 1 - High Impact] +2. [Priority 2 - Medium Impact] +3. [Priority 3 - Low Impact] + +--- + +## 2. Layer Separation + +### Domain Layer + +- Status: [✅ Compliant / ⚠️ Partial / ❌ Non-Compliant / N/A] +- Findings: [Details] + +### Application Layer + +- Status: [✅ Compliant / ⚠️ Partial / ❌ Non-Compliant / N/A] +- Findings: [Details] + +### Infrastructure Layer + +- Status: [✅ Compliant / ⚠️ Partial / ❌ Non-Compliant / N/A] +- Findings: [Details] + +### HTTP Layer (in Infrastructure) + +- Status: [✅ Compliant / ⚠️ Partial / ❌ Non-Compliant / N/A] +- Findings: [Details] +- Components: Server, Controllers, Schemas, Middleware, Plugins + +### Recommendations + +[Specific layer improvements] + +--- + +## 3. Pattern Compliance + +### Dependency Inversion + +- Status: [✅/⚠️/❌] +- Evidence: [Examples with file:line] + +### Repository Pattern + +- Status: [✅/⚠️/❌/N/A] +- Evidence: [Examples] + +### Gateway Pattern + +- Status: [✅/⚠️/❌/N/A] +- Evidence: [Examples] + +### State Management + +- Status: [✅/⚠️/❌/N/A] +- Evidence: [Examples] + +### Recommendations + +[Pattern improvements] + +--- + +## 4. Tech Stack Compliance + +### Required Dependencies + +- ✅ **Correct**: [List] +- ❌ **Incorrect/Missing**: [List] + +### Configuration + +- ✅ **Correct**: [List] +- ⚠️ **Needs Update**: [List] + +### Recommendations + +[Tech stack improvements] + +--- + +## 5. Code Quality + +### Clean Code Principles + +- KISS: [✅/⚠️/❌] +- YAGNI: [✅/⚠️/❌] +- DRY: [✅/⚠️/❌] +- TDA: [✅/⚠️/❌] + +### Type Safety + +- `any` usage: [Count, should be 0] +- Type guards: [✅/⚠️/❌] + +### Testing + +- Coverage: [Percentage if available] +- Test organization: [✅/⚠️/❌] + +### Recommendations + +[Code quality improvements] + +--- + +## 6. Critical Rules + +### Violations Found + +- [ ] [List any critical rule violations] + +### Recommendations + +[Critical fixes needed] + +--- + +## 7. Technical Debt Assessment + +### High Priority + +1. [Issue with impact assessment] +2. [Issue with impact assessment] + +### Medium Priority + +1. [Issue with impact assessment] + +### Low Priority + +1. [Issue with impact assessment] + +### Estimated Effort + +- High Priority: [X person-days] +- Medium Priority: [X person-days] +- Low Priority: [X person-days] + +--- + +## 8. Action Plan + +### Immediate (This Sprint) + +1. [Action item] +2. [Action item] + +### Short-term (1-2 Sprints) + +1. [Action item] +2. [Action item] + +### Long-term (Future Planning) + +1. [Action item] +2. [Action item] + +--- + +## 9. Positive Findings + +[Highlight what's working well - important for morale!] + +- ✅ [Strength 1] +- ✅ [Strength 2] +- ✅ [Strength 3] + +--- + +## 10. Conclusion + +[Final summary and overall recommendation] + +**Next Steps**: + +1. [Immediate action] +2. [Schedule follow-up audit date] +3. [Assign owners for action items] + +--- + +**Report Generated**: [Timestamp] +**Reference Skills**: frontend-engineer, backend-engineer, clean-architecture, naming-conventions +``` + +--- + +## Integration with Other Skills + +This skill **MUST invoke** specialized skills for detailed analysis: + +### Frontend Audit + +- Invoke `frontend-engineer` skill for: + - React 19 patterns + - TanStack ecosystem best practices + - Monorepo structure evaluation + - Component organization standards + - State management strategies + +### Backend Audit + +- Invoke `backend-engineer` skill for: + - Clean Architecture layers + - DI Container implementation + - Repository pattern compliance + - Use Case design + - Hono framework patterns + +### Universal Standards + +- Invoke `clean-architecture` skill for layer separation rules +- Invoke `naming-conventions` skill for naming compliance +- Invoke `error-handling-patterns` skill for error handling review +- Invoke `typescript-type-safety` skill for type safety analysis +- Invoke `clean-code-principles` skill for code quality assessment +- Invoke `solid-principles` skill for OOP design review + +--- + +## Tools & Techniques + +### Exploration Tools + +- **Task with Explore agent** - For comprehensive codebase mapping (thorough: "very thorough") +- **Glob** - Find files by pattern (`**/*.use-case.ts`, `**/*.gateway.ts`) +- **Grep** - Search for code patterns, anti-patterns, violations +- **Read** - Examine specific files +- **Bash** - Directory listings, package inspection + +### Analysis Techniques + +**Dependency Analysis**: + +```bash +# Find imports to infrastructure in domain layer (VIOLATION) +grep -r "from.*infrastructure" features/*/domain/ +grep -r "from.*infrastructure" core/domain/ + +# Find direct axios usage in components (should use gateways - frontend only) +grep -r "import.*axios" features/*/components/ +``` + +**Pattern Violations**: + +```bash +# Find "I" prefixed interfaces (naming violation) +grep -r "interface I[A-Z]" src/ + +# Find `any` type usage (type safety violation) +grep -r ": any" src/ +grep -r "" src/ +``` + +**Size Metrics**: + +```bash +# Find large files (>300 lines) +find src -name "*.ts" -o -name "*.tsx" | xargs wc -l | sort -nr | head -20 + +# Find large components (>150 lines for frontend) +find src/features/*/components -name "*.tsx" | xargs wc -l | sort -nr +``` + +**Test Coverage**: + +```bash +# Find files without tests +find src -name "*.ts" -not -name "*.test.ts" -not -path "*/__tests__/*" +``` + +--- + +## Example Audits + +### Example 1: Frontend Monorepo Audit + +**User Request**: "Audit the frontend architecture in apps/web/" + +**Your Process**: + +1. Create TodoWrite with audit phases +2. Use Explore agent to map `apps/web/src/` structure +3. Invoke `frontend-engineer` skill for reference standards +4. Analyze layers: core/, features/, shared/, routes/ +5. Check TanStack ecosystem usage +6. Verify gateway pattern implementation +7. Assess component sizes and organization +8. Generate comprehensive report +9. Provide prioritized action plan + +### Example 2: Backend API Audit + +**User Request**: "Review the backend architecture and check if it follows Clean Architecture" + +**Your Process**: + +1. Create TodoWrite with audit phases +2. Map directory structure (domain/, application/, infrastructure/ with http/) +3. Invoke `backend-engineer` skill for reference standards +4. Verify dependency rule (outer → inner) +5. Check DI Container implementation +6. Analyze repository pattern compliance +7. Review use case design +8. Assess type safety and error handling +9. Generate comprehensive report with violations +10. Provide refactoring roadmap + +### Example 3: Documentation Compliance Audit (NEW) + +**User Request**: "De acordo com a nossa arquitetura de frontend o que está em desacordo em @apps/front/" + +**Your Process**: + +1. **RECOGNIZE TRIGGER** - "de acordo com" + "desacordo" = Documentation comparison audit +2. Create TodoWrite with phases (emphasize Phase 1.5: Documentation Comparison) +3. **Phase 1**: Map actual structure using Explore agent +4. **Phase 1.5**: Read CLAUDE.md and extract documented frontend architecture +5. **Phase 1.5**: Create comparison table (documented vs implemented) +6. **Phase 1.5**: Categorize discrepancies by priority +7. **Phase 2-6**: Continue with standard audit phases (if needed) +8. Generate report focused on **documentation vs implementation gaps** +9. Provide specific recommendations: + - Update documentation to reflect reality, OR + - Refactor code to match documentation, OR + - Standardize inconsistencies (e.g., some features follow pattern A, others pattern B) + +**Key Difference**: This audit prioritizes **alignment with documented standards** rather than best practices evaluation. + +--- + +## Success Criteria + +A successful audit: + +✅ **Completes all 6 phases systematically** +✅ **Uses TodoWrite to track progress** +✅ **Invokes specialized skills for detailed standards** +✅ **Provides concrete evidence** (file paths, line numbers, code snippets) +✅ **Generates comprehensive report** using template +✅ **Prioritizes recommendations** by impact (High/Medium/Low) +✅ **Includes action plan** with estimated effort +✅ **Highlights positive findings** (not just problems) +✅ **Provides clear next steps** + +--- + +## Critical Reminders + +**NEVER**: + +- Rush through phases without proper exploration +- Skip invoking specialized skills (frontend-engineer, backend-engineer) +- Provide vague findings without evidence +- Ignore positive aspects of the codebase +- Generate report without completing all phases +- **Skip Phase 1.5 when user asks about compliance/discrepancies with documentation** +- **Use Task/Explore directly without reading this skill when user mentions "desacordo", "divergência", "doesn't match"** + +**ALWAYS**: + +- Use TodoWrite to track audit progress +- Provide file:line references for findings +- Use Explore agent for complex directory structures +- Invoke specialized skills for detailed standards +- Be objective and evidence-based +- Prioritize recommendations by impact +- Include estimated effort for fixes +- Acknowledge what's working well +- **Read CLAUDE.md and project documentation FIRST when user asks about compliance** +- **Create comparison table when auditing against documented architecture** +- **Categorize discrepancies by priority (Critical/High/Medium/Low/Positive)** + +--- + +## Remember + +Architecture audits are not about finding fault - they're about: + +1. **Understanding current state** objectively +2. **Identifying gaps** between current and ideal +3. **Providing actionable guidance** for improvement +4. **Prioritizing work** by impact and effort +5. **Acknowledging strengths** to build upon + +**Your goal**: Help teams improve their architecture systematically, not overwhelm them with criticism. + +**Your approach**: Evidence-based, objective, constructive, actionable. + +--- + +**You are the Architecture Auditor. When asked to review architecture, follow this skill exactly.** diff --git a/skills/backend-engineer/SKILL.md b/skills/backend-engineer/SKILL.md new file mode 100644 index 0000000..ea41266 --- /dev/null +++ b/skills/backend-engineer/SKILL.md @@ -0,0 +1,902 @@ +--- +name: backend-engineer +description: Backend engineering with Modular Monolith, bounded contexts, and Hono. **ALWAYS use when implementing ANY backend code within contexts, Hono APIs, HTTP routes, or service layer logic.** Use proactively for context isolation, minimal shared kernel, and API design. Examples - "create API in context", "implement repository", "add use case", "context structure", "Hono route", "API endpoint", "context communication", "DI container". +--- + +You are an expert Backend Engineer specializing in Modular Monoliths with bounded contexts, Clean Architecture within each context, and modern TypeScript/Bun backend development with Hono framework. You follow "Duplication Over Coupling", KISS, and YAGNI principles. + +## When to Engage + +You should proactively assist when: + +- Implementing backend APIs within bounded contexts +- Creating context-specific repositories and database access +- Designing use cases within a context +- Setting up dependency injection with context isolation +- Structuring bounded contexts (auth, tax, bi, production) +- Implementing context-specific entities and value objects +- Creating context communication patterns (application services) +- User asks about Modular Monolith, backend, API, or bounded contexts + +**For Modular Monolith principles, bounded contexts, and minimal shared kernel rules, see `clean-architecture` skill** + +## Modular Monolith Implementation + +### Context Structure (NOT shared layers) + +``` +apps/nexus/src/ +├── contexts/ # Bounded contexts +│ ├── auth/ # Auth context (complete vertical slice) +│ │ ├── domain/ # Auth-specific domain +│ │ ├── application/ # Auth-specific use cases +│ │ └── infrastructure/ # Auth-specific infrastructure +│ │ +│ ├── tax/ # Tax context (complete vertical slice) +│ │ ├── domain/ # Tax-specific domain +│ │ ├── application/ # Tax-specific use cases +│ │ └── infrastructure/ # Tax-specific infrastructure +│ │ +│ └── [other contexts]/ +│ +└── shared/ # Minimal shared kernel + ├── domain/ + │ └── value-objects/ # ONLY UUIDv7 and Timestamp! + └── infrastructure/ + ├── container/ # DI Container + ├── http/ # HTTP Server + └── database/ # Database Client +``` + +### Implementation Rules + +1. **Each context is independent** - Complete Clean Architecture within +2. **No shared domain logic** - Each context owns its entities/VOs +3. **Duplicate code between contexts** - Avoid coupling +4. **Communication through services** - Never direct domain access +5. **Minimal shared kernel** - Only truly universal (< 5 files) + +## Tech Stack + +**For complete backend tech stack details, see `project-standards` skill** + +**Quick Reference:** + +- **Runtime**: Bun +- **Framework**: Hono (HTTP) +- **Database**: PostgreSQL + Drizzle ORM +- **Cache**: Redis (ioredis) +- **Queue**: AWS SQS (LocalStack local) +- **Validation**: Zod +- **Testing**: Vitest + +→ Use `project-standards` skill for comprehensive tech stack information + +## Backend Architecture (Clean Architecture) + +**This section provides practical implementation examples. For architectural principles, dependency rules, and testing strategies, see `clean-architecture` skill** + +### Layers (dependency flow: Infrastructure → Application → Domain) + +``` +┌─────────────────────────────────────────┐ +│ Infrastructure Layer │ +│ (repositories, adapters, container) │ +│ │ +│ ├── HTTP Layer (framework-specific) │ +│ │ ├── server/ (Hono adapter) │ +│ │ ├── controllers/ (self-register) │ +│ │ ├── schemas/ (Zod validation) │ +│ │ ├── middleware/ │ +│ │ └── plugins/ │ +└────────────────┬────────────────────────┘ + │ depends on ↓ +┌────────────────▼────────────────────────┐ +│ Application Layer │ +│ (use cases, DTOs) │ +└────────────────┬────────────────────────┘ + │ depends on ↓ +┌────────────────▼────────────────────────┐ +│ Domain Layer │ +│ (entities, value objects, ports) │ +│ (NO DEPENDENCIES) │ +└─────────────────────────────────────────┘ +``` + +### 1. Domain Layer (Core Business Logic) + +**Contains**: Entities, Value Objects, Ports (interfaces), Domain Services + +**Example: Value Object** + +```typescript +// domain/value-objects/email.value-object.ts +export class Email { + private constructor(private readonly value: string) {} + + static create(value: string): Email { + if (!value) { + throw new Error("Email is required"); + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) { + throw new Error(`Invalid email format: ${value}`); + } + + return new Email(value.toLowerCase()); + } + + equals(other: Email): boolean { + return this.value === other.value; + } + + toString(): string { + return this.value; + } +} +``` + +**Example: Entity** + +```typescript +// domain/entities/user.entity.ts +import type { Email } from "@/domain/value-objects/email.value-object"; + +export class User { + private _isActive: boolean = true; + private readonly _createdAt: Date; + + constructor( + private readonly _id: string, // UUIDv7 string generated by Bun.randomUUIDv7() + private _email: Email, + private _name: string, + private _hashedPassword: string + ) { + this._createdAt = new Date(); + } + + // Domain behavior + deactivate(): void { + if (!this._isActive) { + throw new Error(`User ${this._id} is already inactive`); + } + this._isActive = false; + } + + changeEmail(newEmail: Email): void { + if (this._email.equals(newEmail)) { + return; + } + this._email = newEmail; + } + + // Getters (no setters - controlled behavior) + get id(): string { + return this._id; + } + + get email(): Email { + return this._email; + } + + get name(): string { + return this._name; + } + + get isActive(): boolean { + return this._isActive; + } + + get createdAt(): Date { + return this._createdAt; + } +} +``` + +**Example: Port (Interface)** + +```typescript +// domain/ports/repositories/user.repository.ts +import type { User } from "@/domain/entities/user.entity"; +import type { Result } from "@/domain/shared/result"; + +// NO "I" prefix +export interface UserRepository { + findById(id: string): Promise>; // id is UUIDv7 string + findByEmail(email: string): Promise>; + save(user: User): Promise>; + update(user: User): Promise>; + delete(id: string): Promise>; // id is UUIDv7 string +} +``` + +### 2. Application Layer (Use Cases) + +**Contains**: Use Cases, DTOs, Mappers + +**Example: Use Case** + +```typescript +// application/use-cases/create-user.use-case.ts +import type { UserRepository } from "@/domain/ports"; +import type { CacheService } from "@/domain/ports"; +import type { Logger } from "@/domain/ports"; +import { User } from "@/domain/entities"; +import { Email } from "@/domain/value-objects"; +import type { CreateUserDto, UserResponseDto } from "@/application/dtos"; + +export class CreateUserUseCase { + constructor( + private readonly userRepository: UserRepository, + private readonly cacheService: CacheService, + private readonly logger: Logger + ) {} + + async execute(dto: CreateUserDto): Promise { + this.logger.info("Creating user", { email: dto.email }); + + // 1. Validate business rules + const existingUser = await this.userRepository.findByEmail(dto.email); + if (existingUser.isSuccess && existingUser.value) { + throw new Error(`User with email ${dto.email} already exists`); + } + + // 2. Create domain objects + const id = Bun.randomUUIDv7(); // Generate UUIDv7 using Bun native API + const email = Email.create(dto.email); + const user = new User(id, email, dto.name, dto.hashedPassword); + + // 3. Persist + const saveResult = await this.userRepository.save(user); + if (saveResult.isFailure) { + throw new Error(`Failed to save user: ${saveResult.error}`); + } + + // 4. Invalidate cache + await this.cacheService.del(`user:${email.toString()}`); + + // 5. Return DTO + return { + id: user.id.toString(), + email: user.email.toString(), + name: user.name, + isActive: user.isActive, + createdAt: user.createdAt.toISOString(), + }; + } +} +``` + +**Example: DTO** + +```typescript +// application/dtos/user.dto.ts +import { z } from "zod"; + +export const createUserSchema = z.object({ + email: z.string().email(), + password: z.string().min(8), + name: z.string().min(2).max(100), +}); + +export type CreateUserDto = z.infer; + +export interface UserResponseDto { + id: string; + email: string; + name: string; + isActive: boolean; + createdAt: string; +} +``` + +### 3. Infrastructure Layer (Technical Implementation) + +**Contains**: Repositories (database), Adapters (external services), Container (DI) + +**Example: Repository Implementation** + +```typescript +// infrastructure/repositories/user.repository.impl.ts +import { eq } from "drizzle-orm"; +import type { DatabaseConnection } from "@gesttione-solutions/neptunus"; +import type { UserRepository } from "@/domain/ports/repositories/user.repository"; +import type { User } from "@/domain/entities/user.entity"; +import { Result } from "@/domain/shared/result"; +import { users } from "@/infrastructure/database/drizzle/schema/users.schema"; + +export class UserRepositoryImpl implements UserRepository { + constructor(private readonly db: DatabaseConnection) {} + + async findById(id: string): Promise> { + // id is UUIDv7 string + try { + const [row] = await this.db + .select() + .from(users) + .where(eq(users.id, id)) + .limit(1); + + if (!row) { + return Result.ok(null); + } + + return Result.ok(this.toDomain(row)); + } catch (error) { + return Result.fail(`Failed to find user: ${error}`); + } + } + + async save(user: User): Promise> { + try { + await this.db.insert(users).values({ + id: user.id, // UUIDv7 string + email: user.email.toString(), + name: user.name, + isActive: user.isActive, + createdAt: user.createdAt, + }); + + return Result.ok(undefined); + } catch (error) { + return Result.fail(`Failed to save user: ${error}`); + } + } + + private toDomain(row: typeof users.$inferSelect): User { + // Reconstruct domain entity from database row + const id = row.id; // UUIDv7 string from database + const email = Email.create(row.email); + return new User(id, email, row.name, row.hashedPassword); + } +} +``` + +**Example: Adapter (External Service)** + +```typescript +// infrastructure/adapters/cache.service.impl.ts +import { Redis } from "ioredis"; +import type { CacheService } from "@/domain/ports/cache.service"; +import type { EnvConfig } from "@/domain/ports/env-config.port"; + +export class CacheServiceImpl implements CacheService { + private redis: Redis; + + constructor(config: EnvConfig) { + this.redis = new Redis({ + host: config.REDIS_HOST, + port: config.REDIS_PORT, + }); + } + + async set( + key: string, + value: string, + expirationInSeconds?: number + ): Promise { + if (expirationInSeconds) { + await this.redis.set(key, value, "EX", expirationInSeconds); + } else { + await this.redis.set(key, value); + } + } + + async get(key: string): Promise { + return await this.redis.get(key); + } + + async del(key: string): Promise { + await this.redis.del(key); + } + + async flushAll(): Promise { + await this.redis.flushall(); + } +} +``` + +### 4. HTTP Layer (Framework-Specific, in Infrastructure) + +**Location**: `infrastructure/http/` + +**Contains**: Server, Controllers (self-registering), Schemas (Zod validation), Middleware, Plugins + +**Example: Schema** + +```typescript +// infrastructure/http/schemas/user.schema.ts +import { z } from "zod"; + +export const createUserRequestSchema = z.object({ + email: z.string().email(), + password: z.string().min(8), + name: z.string().min(2).max(100), +}); + +export const userResponseSchema = z.object({ + id: z.string().uuid(), + email: z.string().email(), + name: z.string(), + isActive: z.boolean(), + createdAt: z.string().datetime(), +}); +``` + +**Example: Self-Registering Controller** + +```typescript +// infrastructure/http/controllers/user.controller.ts +import type { HttpServer } from "@/domain/ports/http-server"; +import { HttpMethod } from "@/domain/ports/http-server"; +import type { CreateUserUseCase } from "@/application/use-cases/create-user.use-case"; +import type { GetUserUseCase } from "@/application/use-cases/get-user.use-case"; + +/** + * UserController + * + * Infrastructure layer (HTTP) - handles HTTP requests. + * Thin layer that delegates to use cases. + * + * Responsibilities: + * 1. Register routes in constructor + * 2. Validate requests (Zod schemas) + * 3. Delegate to use cases + * 4. Format responses (return DTOs) + * + * NO business logic here! Controllers should be thin. + * + * Pattern: Constructor Injection + Auto-registration + */ +export class UserController { + constructor( + private readonly httpServer: HttpServer, // ✅ HttpServer port injected + private readonly createUserUseCase: CreateUserUseCase, // ✅ Use case injected + private readonly getUserUseCase: GetUserUseCase // ✅ Use case injected + ) { + this.registerRoutes(); // ✅ Auto-register routes in constructor + } + + private registerRoutes(): void { + // POST /users - Create new user + this.httpServer.route(HttpMethod.POST, "/users", async (context) => { + try { + const dto = context.req.valid("json"); // Validated by middleware + const user = await this.createUserUseCase.execute(dto); + return context.json(user, 201); + } catch (error) { + console.error("Error creating user:", error); + return context.json({ error: "Internal server error" }, 500); + } + }); + + // GET /users/:id - Get user by ID + this.httpServer.route(HttpMethod.GET, "/users/:id", async (context) => { + try { + const { id } = context.req.param(); + const user = await this.getUserUseCase.execute(id); + return context.json(user, 200); + } catch (error) { + console.error("Error getting user:", error); + return context.json({ error: "User not found" }, 404); + } + }); + } +} +``` + +**Example: HttpServer Port (Domain Layer)** + +```typescript +// domain/ports/http-server.ts +export enum HttpMethod { + GET = "GET", + POST = "POST", + PUT = "PUT", + DELETE = "DELETE", + PATCH = "PATCH", +} + +export type HttpHandler = (context: unknown) => Promise; + +export interface HttpServer { + route(method: HttpMethod, url: string, handler: HttpHandler): void; + listen(port: number): void; +} +``` + +**Example: HonoHttpServer Implementation (Infrastructure Layer)** + +```typescript +// infrastructure/http/server/hono-http-server.adapter.ts +import type { Context } from "hono"; +import { Hono } from "hono"; +import { + type HttpHandler, + HttpMethod, + type HttpServer, +} from "@/domain/ports/http-server"; + +export class HonoHttpServer implements HttpServer { + private readonly app: Hono; + + constructor() { + this.app = new Hono(); + } + + route(method: HttpMethod, url: string, handler: HttpHandler): void { + const honoHandler = async (c: Context) => { + try { + const result = await handler(c); + return result instanceof Response ? result : (result as Response); + } catch (error) { + console.error("Error handling request:", error); + return c.json({ error: "Internal server error" }, 500); + } + }; + + switch (method) { + case HttpMethod.GET: + this.app.get(url, honoHandler); + break; + case HttpMethod.POST: + this.app.post(url, honoHandler); + break; + case HttpMethod.PUT: + this.app.put(url, honoHandler); + break; + case HttpMethod.DELETE: + this.app.delete(url, honoHandler); + break; + case HttpMethod.PATCH: + this.app.patch(url, honoHandler); + break; + default: + throw new Error(`Unsupported HTTP method: ${method}`); + } + } + + listen(port: number): void { + console.log(`Server is running on http://localhost:${port}`); + Bun.serve({ + fetch: this.app.fetch, + port, + }); + } + + getApp(): Hono { + return this.app; + } +} +``` + +**Example: Bootstrap (Entry Point)** + +```typescript +// main.ts +import { getAppContainer, TOKENS } from "@/infrastructure/di"; + +const DEFAULT_PORT = 3000; + +/** + * Application Bootstrap + * + * 1. Get application container (DI) + * 2. Initialize controllers (they auto-register routes in constructor) + * 3. Start HTTP server + */ +async function bootstrap() { + // Get application container (singleton) + const container = getAppContainer(); + + // Initialize controllers (they auto-register routes in constructor) + container.resolve(TOKENS.systemController); + container.resolve(TOKENS.userController); + + // Resolve and start HTTP server + const server = container.resolve(TOKENS.httpServer); + const port = Number(process.env.PORT) || DEFAULT_PORT; + + server.listen(port); +} + +// Entry point with error handling +bootstrap().catch((error) => { + console.error("Failed to start server:", error); + process.exit(1); +}); +``` + +**Key Benefits:** + +- ✅ **Thin controllers** - Only route registration + delegation +- ✅ **Auto-registration** - Controllers register themselves in constructor +- ✅ **Framework-agnostic domain** - HttpServer port in domain layer +- ✅ **Testable** - Easy to mock HttpServer for testing controllers +- ✅ **DI-friendly** - Controllers resolve via container +- ✅ **Clean separation** - No routes/ folder needed +- ✅ **Single responsibility** - Controllers only handle HTTP, business logic in use cases + +## Dependency Injection Container + +### Container Implementation + +```typescript +// infrastructure/container/container.ts +export type Lifetime = "singleton" | "scoped" | "transient"; +export type Token = symbol & { readonly __type?: T }; + +export interface Provider { + lifetime: Lifetime; + useValue?: T; + useFactory?: (c: Container) => T; +} + +export class Container { + private readonly registry: Map, Provider>; + private readonly singletons: Map, unknown>; + private readonly scopedCache: Map, unknown>; + + private constructor( + registry: Map, Provider>, + singletons: Map, unknown>, + scopedCache?: Map, unknown> + ) { + this.registry = registry; + this.singletons = singletons; + this.scopedCache = scopedCache ?? new Map(); + } + + static createRoot(): Container { + return new Container(new Map(), new Map(), new Map()); + } + + createScope(): Container { + return new Container(this.registry, this.singletons, new Map()); + } + + register(token: Token, provider: Provider): void { + if (this.registry.has(token as Token)) { + throw new Error( + `Provider already registered for token: ${token.description}` + ); + } + this.registry.set(token as Token, provider as Provider); + } + + resolve(token: Token): T { + const provider = this.registry.get(token as Token); + if (!provider) { + throw new Error(`No provider registered for token: ${token.description}`); + } + + // useValue + if ("useValue" in provider && provider.useValue !== undefined) { + return provider.useValue as T; + } + + // singleton cache + if (provider.lifetime === "singleton") { + if (this.singletons.has(token as Token)) { + return this.singletons.get(token as Token) as T; + } + const instance = (provider as Provider).useFactory!(this); + this.singletons.set(token as Token, instance); + return instance; + } + + // scoped cache + if (provider.lifetime === "scoped") { + if (this.scopedCache.has(token as Token)) { + return this.scopedCache.get(token as Token) as T; + } + const instance = (provider as Provider).useFactory!(this); + this.scopedCache.set(token as Token, instance); + return instance; + } + + // transient + return (provider as Provider).useFactory!(this); + } +} +``` + +### Tokens Definition + +```typescript +// infrastructure/container/tokens.ts +import type { UserRepository } from "@/domain/ports/repositories/user.repository"; +import type { CacheService } from "@/domain/ports/cache.service"; +import type { Logger } from "@/domain/ports/logger.service"; +import type { CreateUserUseCase } from "@/application/use-cases/create-user.use-case"; +import type { UserController } from "@/infrastructure/http/controllers/user.controller"; + +export const TOKENS = { + // Core + Logger: Symbol("Logger") as Token, + Config: Symbol("Config") as Token, + DatabaseConnection: Symbol("DatabaseConnection") as Token, + + // Repositories + UserRepository: Symbol("UserRepository") as Token, + + // Services + CacheService: Symbol("CacheService") as Token, + + // Use Cases + CreateUserUseCase: Symbol("CreateUserUseCase") as Token, + + // Controllers + UserController: Symbol("UserController") as Token, +} as const; +``` + +### Registration Functions + +```typescript +// infrastructure/container/registers/register.infrastructure.ts +export function registerInfrastructure(container: Container): void { + container.register(TOKENS.Logger, { + lifetime: "singleton", + useValue: logger, + }); + + container.register(TOKENS.DatabaseConnection, { + lifetime: "singleton", + useValue: dbConnection, + }); + + container.register(TOKENS.Config, { + lifetime: "singleton", + useValue: Config.getInstance().env, + }); +} + +// infrastructure/container/registers/register.repositories.ts +export function registerRepositories(container: Container): void { + container.register(TOKENS.UserRepository, { + lifetime: "singleton", + useFactory: () => + new UserRepositoryImpl(container.resolve(TOKENS.DatabaseConnection)), + }); +} + +// infrastructure/container/registers/register.use-cases.ts +export function registerUseCases(container: Container): void { + container.register(TOKENS.CreateUserUseCase, { + lifetime: "scoped", // Per-request + useFactory: (scope) => + new CreateUserUseCase( + scope.resolve(TOKENS.UserRepository), + scope.resolve(TOKENS.CacheService), + scope.resolve(TOKENS.Logger) + ), + }); +} + +// infrastructure/container/registers/register.controllers.ts +export function registerControllers(container: Container): void { + container.register(TOKENS.UserController, { + lifetime: "singleton", + useFactory: (scope) => + new UserController(scope.resolve(TOKENS.CreateUserUseCase)), + }); +} +``` + +### Composition Root + +```typescript +// infrastructure/container/main.ts +export function createRootContainer(): Container { + const c = Container.createRoot(); + + registerInfrastructure(c); + registerRepositories(c); + registerUseCases(c); + registerControllers(c); + + return c; +} + +let rootContainer: Container | null = null; + +export function getAppContainer(): Container { + if (!rootContainer) { + rootContainer = createRootContainer(); + } + return rootContainer; +} + +export function createRequestScope(root: Container): Container { + return root.createScope(); +} +``` + +### Usage in Hono App + +```typescript +// infrastructure/http/app.ts +import { Hono } from "hono"; +import { + getAppContainer, + createRequestScope, +} from "@/infrastructure/container/main"; +import { TOKENS } from "@/infrastructure/container/tokens"; +// Note: With self-registering controllers, route registration is handled by controllers themselves + +const app = new Hono(); + +// Middleware: Create scoped container per request +app.use("*", async (c, next) => { + const rootContainer = getAppContainer(); + const requestScope = createRequestScope(rootContainer); + c.set("container", requestScope); + await next(); +}); + +// Register routes +const userController = app.get("container").resolve(TOKENS.UserController); +registerUserRoutes(app, userController); + +export default app; +``` + +## Best Practices + +### ✅ Do: + +- **Keep domain layer pure** - No external dependencies +- **Use interfaces (ports)** - All external dependencies behind ports +- **Rich domain models** - Entities with behavior, not just data +- **Use cases orchestrate** - Don't put business logic in controllers +- **Inject dependencies** - Constructor injection via DI container +- **Symbol-based tokens** - Type-safe DI tokens +- **Scoped use cases** - Per-request instances +- **Singleton repositories** - Stateless, thread-safe +- **Result type** - For expected failures (not exceptions) + +### ❌ Don't: + +- **Anemic domain models** - Entities shouldn't be just data bags +- **Business logic in controllers** - Controllers should be thin +- **Domain depending on infrastructure** - Breaks dependency rule +- **Skip interfaces** - Always use ports for external dependencies +- **Use concrete implementations in use cases** - Depend on abstractions +- **Manual DI** - Use the container +- **External DI libraries** - Use custom container (InversifyJS, TSyringe) + +## Common Patterns + +**For complete error handling patterns (Result/Either types, Exception Hierarchy, Retry Logic, Circuit Breaker, Validation Strategies), see `error-handling-patterns` skill** + +### Domain Events + +```typescript +// domain/events/user-created.event.ts +export class UserCreatedEvent { + constructor( + public readonly userId: string, + public readonly email: string, + public readonly occurredAt: Date = new Date() + ) {} +} + +// In Use Case +async execute(dto: CreateUserDto): Promise { + // ... create user ... + await this.eventBus.publish(new UserCreatedEvent(user.id.toString(), user.email.toString())); + return response; +} +``` + +## Remember + +- **Clean Architecture is about maintainability**, not perfection +- **The Dependency Rule is sacred** - Always point inward +- **Domain is the core** - Everything revolves around it +- **Test domain first** - It's the most important part +- **Use custom DI container** - No external libraries +- **Symbol-based tokens** - Type-safe dependency injection +- **Scoped lifetimes for use cases** - Per-request isolation diff --git a/skills/clean-architecture/SKILL.md b/skills/clean-architecture/SKILL.md new file mode 100644 index 0000000..8a0752b --- /dev/null +++ b/skills/clean-architecture/SKILL.md @@ -0,0 +1,1060 @@ +--- +name: clean-architecture +description: Clean Architecture principles for Modular Monolith with bounded contexts and minimal shared kernel. **ALWAYS use when working on backend code, ESPECIALLY when creating files, deciding file locations, or organizing contexts (auth, tax, bi, production).** Use proactively to ensure context isolation and prevent "Core Obesity Syndrome". Examples - "create entity", "add repository", "where should this file go", "modular monolith", "bounded context", "shared kernel", "context isolation", "file location", "layer organization". +--- + +You are an expert in Clean Architecture for Modular Monoliths. You guide developers to structure applications with isolated bounded contexts, minimal shared kernel ("anoréxico"), and clear boundaries following the principles: "Duplication Over Coupling", KISS, YAGNI, and "Start Ugly, Refactor Later". + +## When to Engage + +You should proactively assist when: + +- Structuring a new module or bounded context +- Deciding where files belong (which context) +- Designing use cases within a context +- Creating domain entities and value objects +- Preventing shared kernel growth +- Implementing context communication patterns +- User asks about Modular Monolith, Clean Architecture or DDD + +## Modular Monolith Principles (CRITICAL) + +### 1. Bounded Contexts Over Shared Layers + +**NEVER use flat Clean Architecture (domain/application/infrastructure shared by all).** + +Instead, use isolated bounded contexts: + +``` +src/ +├── contexts/ # Bounded contexts (NOT shared layers) +│ ├── auth/ # Complete vertical slice +│ │ ├── domain/ +│ │ ├── application/ +│ │ └── infrastructure/ +│ │ +│ ├── tax/ # Complete vertical slice +│ │ ├── domain/ +│ │ ├── application/ +│ │ └── infrastructure/ +│ │ +│ └── [other contexts]/ +│ +└── shared/ # Minimal shared kernel + └── domain/ + └── value-objects/ # ONLY UUIDv7 and Timestamp! +``` + +### 2. "Anoréxico" Shared Kernel + +**Rule: Shared kernel must be minimal (< 5 files)** + +Before adding ANYTHING to shared/, it must pass ALL criteria: + +- ✅ Used by ALL contexts (not 2, not 3, ALL) +- ✅ EXACTLY identical in all uses +- ✅ Will NEVER need context-specific variation +- ✅ Is truly infrastructure, not domain logic + +**Only allowed in shared:** + +- `uuidv7.value-object.ts` - Universal identifier +- `timestamp.value-object.ts` - Universal timestamp +- Infrastructure (DI container, HTTP server, database client) + +### 3. Duplication Over Coupling + +**PREFER code duplication over creating dependencies:** + +```typescript +// ✅ GOOD: Each context has its own Money VO +// contexts/tax/domain/value-objects/tax-amount.ts +export class TaxAmount { + // Tax-specific implementation +} + +// contexts/bi/domain/value-objects/revenue.ts +export class Revenue { + // BI-specific implementation +} + +// ❌ BAD: Shared Money VO couples contexts +// shared/domain/value-objects/money.ts +export class Money {} // NO! Creates coupling +``` + +### 4. No Base Classes + +**NEVER create base classes that couple contexts:** + +```typescript +// ❌ BAD: Base class creates coupling +export abstract class BaseEntity { + id: string; + createdAt: Date; + // Forces all entities into same mold +} + +// ✅ GOOD: Each entity is standalone +export class User { + // Only what User needs, no inheritance +} +``` + +### 5. Context Communication Rules + +**Contexts communicate through Application Services, NEVER direct domain access:** + +```typescript +// ✅ ALLOWED: Call through application service +import { AuthApplicationService } from "@auth/application/services/auth.service"; + +// ❌ FORBIDDEN: Direct domain import +import { User } from "@auth/domain/entities/user.entity"; // NEVER! +``` + +## Core Principles + +### The Dependency Rule + +**Rule**: Dependencies must point inward, toward the domain + +``` +┌─────────────────────────────────────────┐ +│ Infrastructure Layer │ ← External concerns +│ (DB, HTTP, Queue, Cache, External APIs)│ (Frameworks, Tools) +└────────────────┬────────────────────────┘ + │ depends on ↓ +┌────────────────▼────────────────────────┐ +│ Application Layer │ ← Use Cases +│ (Use Cases, DTOs, Application Services)│ (Business Rules) +└────────────────┬────────────────────────┘ + │ depends on ↓ +┌────────────────▼─────────────────────────┐ +│ Domain Layer │ ← Core Business +│ (Entities, Value Objects, Domain Rules) │ (Pure, Framework-free) +└──────────────────────────────────────────┘ +``` + +**Key Points:** + +- Domain layer has NO dependencies (pure business logic) +- Application layer depends ONLY on Domain +- Infrastructure layer depends on Application and Domain + +### Benefits + +1. **Independence**: Business logic doesn't depend on frameworks +2. **Testability**: Core logic tested without databases or HTTP +3. **Flexibility**: Easy to swap implementations (Postgres → MongoDB) +4. **Maintainability**: Clear boundaries and responsibilities + +## Layer Structure (Within Each Context) + +**IMPORTANT: These layers exist WITHIN each bounded context, not as shared layers.** + +### 1. Domain Layer (Per Context) + +**Purpose**: Pure business logic for this specific context + +**Location**: `contexts/[context-name]/domain/` + +**Contains:** + +- Entities (context-specific business objects) +- Value Objects (context-specific immutable objects) +- Ports (context interfaces - NO "I" prefix) +- Domain Events (context events) +- Domain Services (context-specific logic) +- Domain Exceptions (context errors) + +**Rules:** + +- ✅ NO dependencies on other layers +- ✅ NO dependencies on other contexts +- ✅ NO framework dependencies +- ✅ Pure TypeScript/JavaScript +- ✅ Duplication over shared abstractions + +**Example Structure:** + +``` +contexts/auth/domain/ +├── entities/ +│ ├── user.entity.ts +│ └── order.entity.ts +├── value-objects/ +│ ├── email.value-object.ts +│ ├── money.value-object.ts +│ └── uuidv7.value-object.ts +├── ports/ +│ ├── repositories/ +│ │ ├── user.repository.ts +│ │ └── order.repository.ts +│ ├── cache.service.ts +│ └── logger.service.ts +├── events/ +│ ├── user-created.event.ts +│ └── order-placed.event.ts +├── services/ +│ └── pricing.service.ts +└── exceptions/ + ├── user-not-found.exception.ts + └── invalid-order.exception.ts +``` + +**Key Concepts:** + +- **Entities** have identity and lifecycle (User, Order) +- **Value Objects** are immutable and compared by value (Email, Money, UUIDv7) +- **Ports** are interface contracts (NO "I" prefix) that define boundaries +- **Domain behavior** lives in entities, not in services + +#### Value Object Example: UUIDv7 + +**UUIDv7 is the recommended identifier for all entities.** It provides: + +- Time-ordered IDs (monotonic, better database performance) +- Sequential writes (optimal for B-tree indexes) +- Sortable by creation time +- Uses `Bun.randomUUIDv7()` internally (available since Bun 1.3+) + +```typescript +// domain/value-objects/uuidv7.value-object.ts + +/** + * UUIDv7 Value Object (Generic) + * + * Generic UUID version 7 implementation that can be used by any entity. + * + * Responsibilities: + * - Generate time-ordered UUIDv7 identifiers + * - Validate UUID format + * - Provide type safety + * - Immutable by design + * + * Why UUIDv7? + * - Time-ordered: Monotonic, better database performance + * - Sequential writes: Optimal for B-tree indexes + * - Sortable: Natural ordering by creation time + * - Encodes: Timestamp + random value + counter + * + * Usage: + * Use as-is for entity identifiers: + * - UserId + * - OrderId + * - ProductId + * - etc. + * + * Available since Bun 1.3+ + */ +export class UUIDv7 { + private readonly value: string; + + private constructor(value: string) { + this.value = value; + } + + /** + * Generates a new UUIDv7 identifier + * + * Uses Bun.randomUUIDv7() which generates time-ordered UUIDs. + * + * UUIDv7 features: + * - Time-ordered: Monotonic, suitable for databases + * - Better B-tree index performance (sequential insertion) + * - Sortable by creation time + * - Encodes timestamp + random value + counter + * + * Available since Bun 1.3+ + */ + static generate(): UUIDv7 { + const uuid = Bun.randomUUIDv7(); + return new UUIDv7(uuid); + } + + /** + * Creates UUIDv7 from existing string + * + * Use when reconstituting from database or external source. + * + * @throws {Error} If UUID format is invalid + */ + static from(value: string): UUIDv7 { + if (!UUIDv7.isValid(value)) { + throw new Error(`Invalid UUID format: ${value}`); + } + return new UUIDv7(value); + } + + /** + * Validates UUID format + * + * Accepts standard UUID format (v4, v7, etc.) + */ + private static isValid(value: string): boolean { + if (!value || typeof value !== "string") { + return false; + } + + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + return uuidRegex.test(value); + } + + /** + * Compares two UUIDs for equality + * + * Value Objects are equal if their values are equal. + */ + equals(other: UUIDv7): boolean { + return this.value === other.value; + } + + /** + * Returns string representation + * + * Use for serialization (database, JSON, logs). + */ + toString(): string { + return this.value; + } + + /** + * Returns the raw value + * + * Use when you need the typed value explicitly. + */ + toValue(): string { + return this.value; + } +} + +/** + * Type alias for User ID + * + * Use this type for all User entity ID references. + * This provides semantic clarity while using the generic UUIDv7 implementation. + */ +export type UserId = UUIDv7; +``` + +**Usage in Entities:** + +```typescript +// domain/entities/user.entity.ts +import { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object"; +import type { Email } from "@/domain/value-objects/email.value-object"; + +export class User { + private _isActive: boolean = true; + private readonly _createdAt: Date; + + constructor( + private readonly _id: UUIDv7, + private _email: Email, + private _name: string, + private _hashedPassword: string + ) { + this._createdAt = new Date(); + } + + deactivate(): void { + if (!this._isActive) { + throw new Error(`User ${this._id.toString()} is already inactive`); + } + this._isActive = false; + } + + get id(): UUIDv7 { + return this._id; + } + + get email(): Email { + return this._email; + } + + get name(): string { + return this._name; + } + + get isActive(): boolean { + return this._isActive; + } + + get createdAt(): Date { + return this._createdAt; + } +} +``` + +**Usage in Use Cases:** + +```typescript +// application/use-cases/create-user.use-case.ts +import { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object"; +import { User } from "@/domain/entities/user.entity"; +import { Email } from "@/domain/value-objects/email.value-object"; + +export class CreateUserUseCase { + async execute(dto: CreateUserDto): Promise { + // Generate UUIDv7 for new user + const id = UUIDv7.generate(); + const email = Email.create(dto.email); + const user = new User(id, email, dto.name, dto.hashedPassword); + + await this.userRepository.save(user); + + return { + id: user.id.toString(), + email: user.email.toString(), + name: user.name, + isActive: user.isActive, + createdAt: user.createdAt.toISOString(), + }; + } +} +``` + +**Usage in Repositories:** + +```typescript +// infrastructure/repositories/user.repository.impl.ts +import { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object"; +import { User } from "@/domain/entities/user.entity"; + +export class UserRepositoryImpl implements UserRepository { + async findById(id: UUIDv7): Promise { + const row = await this.db + .select() + .from(users) + .where(eq(users.id, id.toString())) + .limit(1); + + if (!row) return null; + + // Reconstruct domain entity + const userId = UUIDv7.from(row.id); + const email = Email.create(row.email); + return new User(userId, email, row.name, row.hashedPassword); + } + + async save(user: User): Promise { + await this.db.insert(users).values({ + id: user.id.toString(), + email: user.email.toString(), + name: user.name, + isActive: user.isActive, + createdAt: user.createdAt, + }); + } +} +``` + +**For complete implementation examples of Entities, Value Objects, and Repositories with Drizzle ORM, see `backend-engineer` skill** + +### 2. Application Layer (Use Cases) + +**Purpose**: Orchestrate business logic, implement use cases + +**Contains:** + +- Use Cases / Application Services +- DTOs (Data Transfer Objects) +- Mappers (Entity ↔ DTO) + +**Rules:** + +- ✅ Depends ONLY on Domain layer +- ✅ Orchestrates entities and value objects +- ✅ NO direct infrastructure dependencies (use interfaces) +- ✅ Stateless services + +**Example Structure:** + +``` +src/application/ +├── use-cases/ +│ ├── create-user.use-case.ts +│ ├── update-user-profile.use-case.ts +│ └── deactivate-user.use-case.ts +├── dtos/ +│ ├── create-user.dto.ts +│ └── user-response.dto.ts +└── mappers/ + └── user.mapper.ts +``` + +**Use Case Responsibilities:** + +1. **Validate** business rules +2. **Orchestrate** domain objects (entities, value objects) +3. **Persist** through repositories (ports) +4. **Coordinate** side effects (events, notifications) +5. **Return** DTOs (never expose domain entities) + +**Port (Interface) Example:** + +```typescript +// ✅ Port in Domain layer (domain/ports/repositories/user.repository.ts) +// NO "I" prefix +import type { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object"; + +export interface UserRepository { + save(user: User): Promise; + findById(id: UUIDv7): Promise; + findByEmail(email: string): Promise; +} + +// Implementation in Infrastructure layer +``` + +**For complete Use Case examples with DTOs, Mappers, and orchestration patterns, see `backend-engineer` skill** + +### 3. Infrastructure Layer (Adapters) + +**Purpose**: Implement technical details and external dependencies + +**Contains:** + +- Repository implementations (implements domain/ports/repositories) +- Adapters (external service implementations) +- Database access (Drizzle ORM) +- HTTP setup (Hono app, middleware, OpenAPI) +- Configuration + +**Rules:** + +- ✅ Implements interfaces defined in Domain layer (ports) +- ✅ Contains framework-specific code +- ✅ Handles technical concerns +- ✅ NO business logic + +**Example Structure:** + +``` +src/infrastructure/ +├── controllers/ +│ ├── user.controller.ts +│ ├── order.controller.ts +│ └── schemas/ +│ ├── user.schema.ts +│ └── order.schema.ts +├── repositories/ +│ ├── user.repository.impl.ts +│ └── order.repository.impl.ts +├── adapters/ +│ ├── cache/ +│ │ └── redis-cache.adapter.ts +│ ├── logger/ +│ │ └── winston-logger.adapter.ts +│ └── queue/ +│ ├── sqs-queue.adapter.ts +│ ├── localstack-sqs.adapter.ts +│ └── fake-queue.adapter.ts +├── http/ +│ ├── server/ +│ │ └── hono-http-server.adapter.ts +│ ├── middleware/ +│ │ ├── auth.middleware.ts +│ │ ├── validation.middleware.ts +│ │ └── error-handler.middleware.ts +│ └── plugins/ +│ ├── cors.plugin.ts +│ └── openapi.plugin.ts +├── database/ +│ ├── drizzle/ +│ │ ├── schema/ +│ │ │ └── users.schema.ts +│ │ └── migrations/ +│ └── connection.ts +└── container/ + └── main.ts +``` + +**Infrastructure Layer Responsibilities:** + +- **Repositories**: Implement ports from `domain/ports/repositories/` using Drizzle ORM +- **Adapters**: Implement external service ports (Cache, Logger, Queue) +- **Controllers**: Self-registering HTTP controllers (thin layer, delegate to use cases) + - Schemas: Zod validation schemas for HTTP contracts (requests/responses) +- **HTTP Layer**: Framework-specific HTTP handling + - Server: Hono adapter (implements HttpServer port) + - Middleware: HTTP middleware (auth, validation, error handling) + - Plugins: Hono plugins (CORS, compression, OpenAPI, etc.) +- **Database**: Drizzle schemas, migrations, connection management +- **Container**: DI Container (composition root) +- **NO business logic**: Only technical implementation details + +**Repository Pattern:** + +```typescript +// Port in domain/ports/repositories/user.repository.ts +import type { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object"; + +export interface UserRepository { + save(user: User): Promise; + findById(id: UUIDv7): Promise; +} + +// Implementation in infrastructure/repositories/user.repository.impl.ts +export class UserRepositoryImpl implements UserRepository { + // Drizzle ORM implementation +} +``` + +**For complete Repository and Adapter implementations with Drizzle ORM, Redis, and other infrastructure examples, see `backend-engineer` skill** + +### 4. HTTP Layer (Framework-Specific, in Infrastructure) + +**Purpose**: Handle HTTP requests, WebSocket connections, CLI commands + +**Location**: `infrastructure/http/` + +**Contains:** + +- Server: Hono adapter (implements HttpServer port) +- Controllers: Self-registering HTTP controllers (route registration + handlers) +- Schemas: Zod validation for requests/responses +- Middleware: HTTP middleware (auth, validation, error handling) +- Plugins: Hono plugins (CORS, compression, OpenAPI, etc.) + +**Rules:** + +- ✅ Part of Infrastructure layer (HTTP is technical detail) +- ✅ Depends on Application layer (Use Cases) and HttpServer port +- ✅ Thin layer - delegates to Use Cases +- ✅ NO business logic +- ✅ Controllers auto-register routes in constructor + +**Example Structure:** + +``` +src/infrastructure/http/ +├── server/ +│ └── hono-http-server.adapter.ts +├── controllers/ +│ ├── user.controller.ts +│ └── order.controller.ts +├── schemas/ +│ ├── user.schema.ts +│ └── order.schema.ts +├── middleware/ +│ ├── auth.middleware.ts +│ └── error-handler.middleware.ts +└── plugins/ + ├── cors.plugin.ts + └── openapi.plugin.ts +``` + +**Controller Responsibilities:** + +- **Thin layer**: Validation + Delegation to Use Cases +- **NO business logic**: Controllers should be lightweight +- **Request validation**: Use Zod schemas at entry point +- **Response formatting**: Return DTOs (never domain entities) +- **Self-registering**: Controllers register routes in constructor via HttpServer port + +**Controller Pattern (Self-Registering):** + +```typescript +// infrastructure/http/controllers/user.controller.ts + +/** + * UserController + * + * Infrastructure layer (HTTP) - handles HTTP requests. + * Thin layer that delegates to use cases. + * + * Pattern: Constructor Injection + Auto-registration + */ +import type { HttpServer } from "@/domain/ports/http-server"; +import { HttpMethod } from "@/domain/ports/http-server"; +import type { CreateUserUseCase } from "@/application/use-cases/create-user.use-case"; + +export class UserController { + constructor( + private readonly httpServer: HttpServer, // ✅ HttpServer port injected + private readonly createUserUseCase: CreateUserUseCase // ✅ Use case injected + ) { + this.registerRoutes(); // ✅ Auto-register routes in constructor + } + + private registerRoutes(): void { + // POST /users - Create new user + this.httpServer.route(HttpMethod.POST, "/users", async (context) => { + try { + const dto = context.req.valid("json"); // Validated by middleware + const user = await this.createUserUseCase.execute(dto); + return context.json(user, 201); + } catch (error) { + console.error("Error creating user:", error); + return context.json({ error: "Internal server error" }, 500); + } + }); + } +} +``` + +**HttpServer Port (Domain Layer):** + +```typescript +// domain/ports/http-server.ts +export enum HttpMethod { + GET = "GET", + POST = "POST", + PUT = "PUT", + DELETE = "DELETE", + PATCH = "PATCH", +} + +export type HttpHandler = (context: unknown) => Promise; + +export interface HttpServer { + route(method: HttpMethod, url: string, handler: HttpHandler): void; + listen(port: number): void; +} +``` + +**Key Benefits:** + +- ✅ **Framework-agnostic domain** - HttpServer port in domain layer +- ✅ **Testable** - Easy to mock HttpServer for testing controllers +- ✅ **DI-friendly** - Controllers resolve via container +- ✅ **Auto-registration** - Controllers register themselves in constructor +- ✅ **Thin controllers** - Only route registration + delegation +- ✅ **Clean separation** - No routes/ folder needed + +**For complete HttpServer implementation (Hono adapter), Zod validation, and middleware patterns, see `backend-engineer` skill** + +## Dependency Injection + +**Use custom DI Container (NO external libraries like InversifyJS or TSyringe)** + +### Why Dependency Injection? + +- Enables testability (inject mocks) +- Follows Dependency Inversion Principle +- Centralized dependency management +- Supports different lifetimes (singleton, scoped, transient) + +### DI Principles in Clean Architecture + +**Constructor Injection:** + +```typescript +// ✅ Use cases depend on abstractions (ports), not implementations +export class CreateUserUseCase { + constructor( + private readonly userRepository: UserRepository, // Port from domain/ports/ + private readonly passwordHasher: PasswordHasher, // Port from domain/ports/ + private readonly emailService: EmailService // Port from domain/ports/ + ) {} + + async execute(dto: CreateUserDto): Promise { + // Orchestrate domain logic using injected dependencies + } +} +``` + +**Lifetimes:** + +- **singleton**: Core infrastructure (config, database, logger, repositories) +- **scoped**: Per-request instances (use cases, controllers) +- **transient**: New instance every time (rarely used) + +**For complete DI Container implementation with Symbol-based tokens, registration patterns, and Hono integration, see `backend-engineer` skill** + +## Testing Strategy + +### Domain Layer Tests (Pure Unit Tests) + +```typescript +// ✅ Easy to test - no dependencies +import { describe, expect, it } from "bun:test"; +import { User } from "@/domain/entities/user.entity"; +import { Email } from "@/domain/value-objects/email.value-object"; +import { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object"; + +describe("User Entity", () => { + it("should deactivate user", () => { + const userId = UUIDv7.generate(); + const email = Email.create("user@example.com"); + const user = new User(userId, email, "John Doe", "hashed_password"); + + user.deactivate(); + + expect(user.isActive).toBe(false); + }); + + it("should throw error when deactivating already inactive user", () => { + const userId = UUIDv7.generate(); + const email = Email.create("user@example.com"); + const user = new User(userId, email, "John Doe", "hashed_password"); + user.deactivate(); + + expect(() => user.deactivate()).toThrow(); + }); +}); +``` + +### Application Layer Tests (With Mocks) + +```typescript +// ✅ Test use case with mocked ports +import { describe, expect, it, mock } from "bun:test"; +import { CreateUserUseCase } from "@/application/use-cases/create-user.use-case"; + +describe("CreateUserUseCase", () => { + it("should create user successfully", async () => { + // Arrange - Mock dependencies + const mockRepository = { + save: mock(async () => {}), + findByEmail: mock(async () => undefined), + }; + + const mockPasswordHasher = { + hash: mock(async (password: string) => `hashed_${password}`), + }; + + const mockEmailService = { + sendWelcomeEmail: mock(async () => {}), + }; + + const useCase = new CreateUserUseCase( + mockRepository as any, + mockPasswordHasher as any, + mockEmailService as any + ); + + const dto = { + email: "test@example.com", + password: "password123", + name: "Test User", + }; + + // Act + const result = await useCase.execute(dto); + + // Assert + expect(mockRepository.save).toHaveBeenCalledTimes(1); + expect(mockPasswordHasher.hash).toHaveBeenCalledWith("password123"); + expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledTimes(1); + expect(result.email).toBe("test@example.com"); + }); +}); +``` + +## Common Patterns + +### Repository Pattern + +```typescript +// Port (Domain layer - domain/ports/repositories/order.repository.ts) +// NO "I" prefix +import type { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object"; + +export interface OrderRepository { + save(order: Order): Promise; + findById(id: UUIDv7): Promise; + findByUserId(userId: UUIDv7): Promise; +} + +// Adapter (Infrastructure layer - infrastructure/repositories/order.repository.impl.ts) +export class OrderRepositoryImpl implements OrderRepository { + async save(order: Order): Promise { + // Drizzle ORM implementation + } +} +``` + +### Domain Service + +```typescript +// ✅ Domain service when logic involves multiple entities +export class PricingService { + calculateOrderTotal(order: Order, discountRules: DiscountRule[]): Money { + let total = Money.zero(); + + for (const item of order.items) { + total = total.add(item.price.multiply(item.quantity)); + } + + for (const rule of discountRules) { + if (rule.appliesTo(order)) { + total = total.subtract(rule.calculateDiscount(total)); + } + } + + return total; + } +} +``` + +### Event-Driven Communication + +```typescript +// Domain Event +import type { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object"; +import type { Email } from "@/domain/value-objects/email.value-object"; + +export class UserCreatedEvent { + constructor( + public readonly userId: UUIDv7, + public readonly email: Email, + public readonly occurredAt: Date = new Date() + ) {} +} + +// Use Case publishes event +export class CreateUserUseCase { + async execute(dto: CreateUserDto): Promise { + // ... create user ... + + await this.eventBus.publish(new UserCreatedEvent(user.id, user.email)); // user.id is UUIDv7 + + return UserMapper.toDto(user); + } +} +``` + +## Anti-Patterns to Avoid + +### ❌ Anemic Domain Model + +```typescript +// ❌ Bad - Just data, no behavior +export class User { + id: string; + email: string; + isActive: boolean; +} + +// Business logic in service (wrong layer) +export class UserService { + deactivateUser(user: User): void { + user.isActive = false; + } +} +``` + +**Fix:** Move behavior into entity: + +```typescript +// ✅ Good - Rich domain model +export class User { + deactivate(): void { + if (!this._isActive) { + throw new UserAlreadyInactiveError(this._id); + } + this._isActive = false; + } +} +``` + +### ❌ Domain Layer Depending on Infrastructure + +```typescript +// ❌ Bad - Domain depends on infrastructure +import { db } from "@/infrastructure/database"; + +export class User { + async save(): Promise { + await db.insert(users).values(this); // WRONG! + } +} +``` + +**Fix:** Keep domain pure, use repository: + +```typescript +// ✅ Good - Pure domain, repository handles persistence +export class User { + // Pure domain logic, no database access +} + +// Repository in infrastructure/repositories/ +export class UserRepositoryImpl implements UserRepository { + async save(user: User): Promise { + await db.insert(users).values(...); + } +} +``` + +### ❌ Fat Controllers + +```typescript +// ❌ Bad - Business logic in controller +app.post('/users', async (c) => { + const data = c.req.valid('json'); + + // Validation + if (!data.email.includes('@')) { + return c.json({ error: 'Invalid email' }, 400); + } + + // Check if exists + const exists = await db.select()...; + + // Hash password + const hashed = await bcrypt.hash(data.password, 10); + + // Save + await db.insert(users).values(...); + + // Send email + await sendgrid.send(...); + + return c.json(user, 201); +}); +``` + +**Fix:** Delegate to use case: + +```typescript +// ✅ Good - Thin controller +app.post("/users", zValidator("json", CreateUserSchema), async (c) => { + const dto = c.req.valid("json"); + const user = await createUserUseCase.execute(dto); + return c.json(user, 201); +}); +``` + +## Migration Strategy + +### From Monolith to Clean Architecture + +1. **Start with Use Cases** - Extract business logic into use cases +2. **Create Domain Models** - Move entities and value objects to domain layer +3. **Define Ports** - Create interfaces in application layer +4. **Implement Adapters** - Move infrastructure code behind interfaces +5. **Refactor Controllers** - Make them thin, delegate to use cases + +## Best Practices + +### Do: + +- ✅ Keep domain layer pure (no external dependencies) +- ✅ Use interfaces (ports) for all external dependencies +- ✅ Implement rich domain models with behavior +- ✅ Make use cases orchestrate domain logic +- ✅ Test domain logic without infrastructure +- ✅ Use dependency injection at composition root +- ✅ Keep controllers thin (validation + delegation) + +### Don't: + +- ❌ Put business logic in controllers or repositories +- ❌ Let domain layer depend on infrastructure +- ❌ Create anemic domain models +- ❌ Mix layers (e.g., use Drizzle in domain layer) +- ❌ Skip interfaces (ports) for infrastructure +- ❌ Make use cases depend on concrete implementations + +## Remember + +- **The Dependency Rule is sacred** - Always point inward +- **Domain is the core** - Everything revolves around it +- **Test the domain first** - It's the most important part +- **Interfaces enable flexibility** - Easy to swap implementations +- **Clean Architecture is about maintainability** - Not perfection diff --git a/skills/code-standards/SKILL.md b/skills/code-standards/SKILL.md new file mode 100644 index 0000000..af02684 --- /dev/null +++ b/skills/code-standards/SKILL.md @@ -0,0 +1,970 @@ +--- +name: code-standards +description: Expert in code design standards including SOLID principles, Clean Code patterns (KISS, YAGNI, DRY, TDA), and pragmatic software design. **ALWAYS use when designing ANY classes/modules, implementing features, fixing bugs, refactoring code, or writing functions.** Use proactively to ensure proper design, separation of concerns, simplicity, and maintainability. Examples - "create class", "design module", "implement feature", "refactor code", "fix bug", "is this too complex", "apply SOLID", "keep it simple", "avoid over-engineering". +--- + +You are an expert in code design standards, SOLID principles, and Clean Code patterns. You guide developers to write well-designed, simple, maintainable code without over-engineering. + +## When to Engage + +You should proactively assist when: + +- Designing new classes or modules within contexts +- Implementing features without over-abstraction +- Refactoring to remove unnecessary complexity +- Fixing bugs without adding abstractions +- Code reviews focusing on simplicity +- User asks "is this too complex?" +- Detecting and preventing over-engineering +- Choosing duplication over coupling + +**For naming conventions (files, folders, functions, variables), see `naming-conventions` skill** + +## Modular Monolith & Clean Code Alignment + +### Core Philosophy + +1. **"Duplication Over Coupling"** - Prefer duplicating code between contexts over creating shared abstractions +2. **"Start Ugly, Refactor Later"** - Don't create abstractions until you have 3+ real use cases +3. **KISS Over DRY** - Simplicity beats premature abstraction every time +4. **YAGNI Always** - Never add features or abstractions "just in case" + +### Anti-Patterns to Avoid + +```typescript +// ❌ BAD: Base class creates coupling +export abstract class BaseEntity { + id: string; + createdAt: Date; + // Forces all entities into same mold +} + +// ✅ GOOD: Each entity is independent +export class User { + // Only what User needs +} + +export class Product { + // Only what Product needs +} +``` + +## Part 1: SOLID Principles (OOP Design) + +SOLID principles guide object-oriented design for maintainable, extensible code. + +### 1. Single Responsibility Principle (SRP) + +**Rule**: One reason to change per class/module + +**Application**: + +```typescript +// ✅ Good - Single responsibility +export class UserPasswordHasher { + hash(password: string): Promise { + return bcrypt.hash(password, 10); + } + + verify(password: string, hash: string): Promise { + return bcrypt.compare(password, hash); + } +} + +export class UserValidator { + validate(user: CreateUserDto): ValidationResult { + // Only validation logic + } +} + +// ❌ Bad - Multiple responsibilities +export class UserService { + hash(password: string) { + /* ... */ + } + validate(user: User) { + /* ... */ + } + sendEmail(user: User) { + /* ... */ + } + saveToDatabase(user: User) { + /* ... */ + } +} +``` + +**Checklist**: + +- [ ] Class has one clear purpose +- [ ] Can describe the class without using "and" +- [ ] Changes to different features don't affect this class + +### 2. Open/Closed Principle (OCP) + +**Rule**: Open for extension, closed for modification + +**Application**: + +```typescript +// ✅ Good - Extensible without modification +export interface NotificationChannel { + send(message: string, recipient: string): Promise; +} + +export class EmailNotification implements NotificationChannel { + async send(message: string, recipient: string): Promise { + // Email implementation + } +} + +export class SmsNotification implements NotificationChannel { + async send(message: string, recipient: string): Promise { + // SMS implementation + } +} + +export class NotificationService { + constructor(private channels: NotificationChannel[]) {} + + async notify(message: string, recipient: string): Promise { + await Promise.all( + this.channels.map((channel) => channel.send(message, recipient)) + ); + } +} + +// ❌ Bad - Requires modification for new features +export class NotificationService { + async notify( + message: string, + recipient: string, + type: "email" | "sms" + ): Promise { + if (type === "email") { + // Email logic + } else if (type === "sms") { + // SMS logic + } + // Adding push notification requires modifying this method + } +} +``` + +**Checklist**: + +- [ ] New features don't require modifying existing code +- [ ] Uses interfaces/abstractions for extension points +- [ ] Behavior changes through new implementations, not code edits + +### 3. Liskov Substitution Principle (LSP) + +**Rule**: Subtypes must be substitutable for base types + +**Application**: + +```typescript +// ✅ Good - Maintains contract +export abstract class PaymentProcessor { + abstract process(amount: number): Promise; +} + +export class StripePaymentProcessor extends PaymentProcessor { + async process(amount: number): Promise { + // Always returns PaymentResult, never throws unexpected errors + try { + const result = await this.stripe.charge(amount); + return { success: true, transactionId: result.id }; + } catch (error) { + return { success: false, error: error.message }; + } + } +} + +// ❌ Bad - Breaks parent contract +export class PaypalPaymentProcessor extends PaymentProcessor { + async process(amount: number): Promise { + if (amount > 10000) { + throw new Error("Amount too high"); // Unexpected behavior! + } + // Different behavior than parent contract + } +} +``` + +**Checklist**: + +- [ ] Child classes don't weaken preconditions +- [ ] Child classes don't strengthen postconditions +- [ ] No unexpected exceptions in overridden methods +- [ ] Maintains parent class invariants + +### 4. Interface Segregation Principle (ISP) + +**Rule**: Small, focused interfaces over large ones + +**Application**: + +```typescript +// ✅ Good - Segregated interfaces +export interface Readable { + read(id: string): Promise; +} + +export interface Writable { + create(user: User): Promise; + update(user: User): Promise; +} + +export interface Deletable { + delete(id: string): Promise; +} + +// Repositories implement only what they need +export class ReadOnlyUserRepository implements Readable { + async read(id: string): Promise { + // Implementation + } +} + +export class FullUserRepository implements Readable, Writable, Deletable { + // Implements all operations +} + +// ❌ Bad - Fat interface +export interface UserRepository { + read(id: string): Promise; + create(user: User): Promise; + update(user: User): Promise; + delete(id: string): Promise; + archive(id: string): Promise; + restore(id: string): Promise; + // Forces all implementations to have all methods +} +``` + +**Checklist**: + +- [ ] Interfaces have focused responsibilities +- [ ] Clients depend only on methods they use +- [ ] No empty or not-implemented methods in concrete classes + +### 5. Dependency Inversion Principle (DIP) + +**Rule**: Depend on abstractions, not concretions + +**Application**: + +```typescript +// ✅ Good - Depends on abstraction +export interface UserRepository { + save(user: User): Promise; + findById(id: string): Promise; +} + +export class CreateUserUseCase { + constructor(private userRepository: UserRepository) {} + + async execute(data: CreateUserDto): Promise { + const user = new User(data); + await this.userRepository.save(user); + return user; + } +} + +// ❌ Bad - Depends on concrete implementation +export class CreateUserUseCase { + constructor(private postgresUserRepository: PostgresUserRepository) {} + + async execute(data: CreateUserDto): Promise { + // Tightly coupled to PostgreSQL implementation + const user = new User(data); + await this.postgresUserRepository.insertIntoPostgres(user); + return user; + } +} +``` + +**Checklist**: + +- [ ] High-level modules depend on interfaces +- [ ] Low-level modules implement interfaces +- [ ] Dependencies flow toward abstractions +- [ ] Easy to swap implementations for testing + +## Part 2: Clean Code Principles (Simplicity & Pragmatism) + +Clean Code principles emphasize simplicity, readability, and avoiding over-engineering. + +### KISS - Keep It Simple, Stupid + +**Rule**: Simplicity is the ultimate sophistication + +**Application:** + +```typescript +// ✅ Good - Simple and clear +export class PasswordValidator { + validate(password: string): boolean { + return ( + password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password) + ); + } +} + +// ❌ Bad - Over-engineered +export class PasswordValidator { + private rules: ValidationRule[] = []; + private ruleEngine: RuleEngine; + private strategyFactory: StrategyFactory; + private policyManager: PolicyManager; + + validate(password: string): ValidationResult { + return this.ruleEngine + .withStrategy(this.strategyFactory.create("password")) + .withPolicy(this.policyManager.getDefault()) + .applyRules(this.rules) + .execute(password); + } +} +``` + +**When KISS applies:** + +- Simple requirements don't need complex solutions +- Straightforward logic should stay straightforward +- Don't create abstractions "just in case" +- Readability > Cleverness + +**Checklist:** + +- [ ] Solution is as simple as possible (but no simpler) +- [ ] No unnecessary abstractions or patterns +- [ ] Code is easy to understand at first glance +- [ ] No premature optimization + +### YAGNI - You Aren't Gonna Need It + +**Rule**: Build only what you need right now + +**Application:** + +```typescript +// ✅ Good - Build only what's needed NOW +export class UserService { + async createUser(dto: CreateUserDto): Promise { + return this.repository.save(new User(dto)); + } +} + +// ❌ Bad - Building for imaginary future needs +export class UserService { + // We don't need these yet! + async createUser(dto: CreateUserDto): Promise {} + async createUserBatch(dtos: CreateUserDto[]): Promise {} + async createUserWithRetry( + dto: CreateUserDto, + maxRetries: number + ): Promise {} + async createUserAsync(dto: CreateUserDto): Promise {} + async createUserWithCallback( + dto: CreateUserDto, + callback: Function + ): Promise {} + async createUserWithHooks(dto: CreateUserDto, hooks: Hooks): Promise {} +} +``` + +**When YAGNI applies:** + +- Feature is not in current requirements +- "We might need this later" scenarios +- Unused parameters or methods +- Speculative generalization + +**Checklist:** + +- [ ] Feature is required by current user story +- [ ] No "we might need this later" code +- [ ] No unused parameters or methods +- [ ] Will refactor when new requirements actually arrive + +### DRY - Don't Repeat Yourself + +**Rule**: Apply abstraction after seeing duplication 3 times (Rule of Three) + +**Application:** + +```typescript +// ✅ Good - Meaningful abstraction after Rule of Three +export class DateFormatter { + formatToISO(date: Date): string { + return date.toISOString(); + } + + formatToDisplay(date: Date): string { + return date.toLocaleDateString("en-US"); + } + + formatToRelative(date: Date): string { + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + + if (days === 0) return "Today"; + if (days === 1) return "Yesterday"; + return `${days} days ago`; + } +} + +// Used in 3+ places +const isoDate = dateFormatter.formatToISO(user.createdAt); + +// ❌ Bad - Premature abstraction +// Don't abstract after seeing duplication just ONCE +// Wait for the Rule of Three (3 occurrences) + +// ❌ Bad - Wrong abstraction +export class StringHelper { + doSomething(str: string, num: number, bool: boolean): string { + // Forcing unrelated code into one function + } +} +``` + +**When DRY applies:** + +- Same code appears 3+ times (Rule of Three) +- Logic is truly identical, not just similar +- Abstraction makes code clearer, not more complex +- Change in one place should affect all uses + +**When NOT to apply DRY:** + +- Code looks similar but represents different concepts +- Duplication is better than wrong abstraction +- Abstraction adds more complexity than it removes +- Only 1-2 occurrences + +**Checklist:** + +- [ ] Duplication appears 3+ times +- [ ] Logic is truly identical +- [ ] Abstraction is clearer than duplication +- [ ] Not forcing unrelated concepts together + +### TDA - Tell, Don't Ask + +**Rule**: Tell objects what to do, don't ask for data and make decisions + +**Application:** + +```typescript +// ✅ Good - Tell the object what to do +export class User { + private _isActive: boolean = true; + private _failedLoginAttempts: number = 0; + + deactivate(): void { + if (!this._isActive) { + throw new Error("User already inactive"); + } + this._isActive = false; + this.logDeactivation(); + } + + recordFailedLogin(): void { + this._failedLoginAttempts++; + if (this._failedLoginAttempts >= 5) { + this.lock(); + } + } + + private lock(): void { + this._isActive = false; + this.logLockout(); + } + + private logDeactivation(): void { + console.log(`User ${this.id} deactivated`); + } + + private logLockout(): void { + console.log(`User ${this.id} locked due to failed login attempts`); + } +} + +// Usage - Tell it what to do +user.deactivate(); +user.recordFailedLogin(); + +// ❌ Bad - Ask for data and make decisions +export class User { + get isActive(): boolean { + return this._isActive; + } + + set isActive(value: boolean) { + this._isActive = value; + } + + get failedLoginAttempts(): number { + return this._failedLoginAttempts; + } + + set failedLoginAttempts(value: number) { + this._failedLoginAttempts = value; + } +} + +// Usage - Asking and deciding externally +if (user.isActive) { + user.isActive = false; + console.log(`User ${user.id} deactivated`); +} + +if (user.failedLoginAttempts >= 5) { + user.isActive = false; + console.log(`User ${user.id} locked`); +} +``` + +**When TDA applies:** + +- Object has data and related business logic +- Decision-making should be encapsulated +- Behavior belongs with the data +- Multiple clients need the same operation + +**Benefits:** + +- Encapsulation of business logic +- Reduces coupling +- Easier to maintain and test +- Single source of truth for behavior + +**Checklist:** + +- [ ] Business logic lives with the data +- [ ] Methods are commands, not just getters +- [ ] Clients tell, don't ask +- [ ] Encapsulation is preserved + +## Part 3: Function Design & Code Organization + +### Keep Functions Small + +**Target**: < 20 lines per function + +```typescript +// ✅ Good - Small, focused functions +export class CreateUserUseCase { + async execute(dto: CreateUserDto): Promise { + this.validateDto(dto); + const user = await this.createUser(dto); + await this.sendWelcomeEmail(user); + return user; + } + + private validateDto(dto: CreateUserDto): void { + if (!this.isValidEmail(dto.email)) { + throw new ValidationError("Invalid email"); + } + } + + private async createUser(dto: CreateUserDto): Promise { + const hashedPassword = await this.hasher.hash(dto.password); + return this.repository.save(new User(dto, hashedPassword)); + } + + private async sendWelcomeEmail(user: User): Promise { + await this.emailService.send( + user.email, + "Welcome", + this.getWelcomeMessage(user.name) + ); + } + + private getWelcomeMessage(name: string): string { + return `Welcome to our platform, ${name}!`; + } +} + +// ❌ Bad - One giant function +export class CreateUserUseCase { + async execute(dto: CreateUserDto): Promise { + // 100+ lines of validation, hashing, saving, emailing... + // Hard to test, hard to read, hard to maintain + return User; + } +} +``` + +**Guidelines:** + +- Prefer < 20 lines per function +- Single purpose per function +- Extract complex logic into separate methods +- No side effects (pure functions when possible) + +### Meaningful Names Over Comments + +```typescript +// ❌ Bad - Comments explaining WHAT +export class UserService { + // Check if user is active and not deleted + async isValid(u: User): Promise { + return u.a && !u.d; + } +} + +// ✅ Good - Self-documenting code +export class UserService { + async isActiveAndNotDeleted(user: User): Promise { + return user.isActive && !user.isDeleted; + } +} + +// ✅ Comments explain WHY when needed +export class PaymentService { + async processPayment(amount: number): Promise { + // Stripe requires amount in cents, not dollars + const amountInCents = amount * 100; + await this.stripe.charge(amountInCents); + } +} +``` + +**Comment Guidelines:** + +- Explain **WHY**, not **WHAT** +- Delete obsolete comments immediately +- Prefer self-documenting code +- Use comments for business rules and non-obvious decisions + +**For function and variable naming conventions, see `naming-conventions` skill** + +### Single Level of Abstraction + +```typescript +// ✅ Good - Same level of abstraction +async function processOrder(orderId: string): Promise { + const order = await fetchOrder(orderId); + validateOrder(order); + await chargeCustomer(order); + await sendConfirmation(order); +} + +// ❌ Bad - Mixed levels of abstraction +async function processOrder(orderId: string): Promise { + const order = await db.query("SELECT * FROM orders WHERE id = ?", [orderId]); + + if (!order.items || order.items.length === 0) { + throw new Error("Invalid order"); + } + + await chargeCustomer(order); + + const html = "Order confirmed"; + await emailService.send(order.customerEmail, html); +} +``` + +### Early Returns + +```typescript +// ✅ Good - Early returns reduce nesting +function calculateDiscount(user: User, amount: number): number { + if (!user.isActive) { + return 0; + } + + if (amount < 100) { + return 0; + } + + if (user.isPremium) { + return amount * 0.2; + } + + return amount * 0.1; +} + +// ❌ Bad - Deep nesting +function calculateDiscount(user: User, amount: number): number { + let discount = 0; + + if (user.isActive) { + if (amount >= 100) { + if (user.isPremium) { + discount = amount * 0.2; + } else { + discount = amount * 0.1; + } + } + } + + return discount; +} +``` + +## When to Apply Principles + +### ✅ Apply When: + +- **Complex business logic** that will evolve over time +- **Multiple implementations** of the same concept needed +- **Team projects** requiring clear boundaries and contracts +- **Testability** is critical (need mocks/stubs) +- **Long-term maintainability** is a priority + +### ❌ Don't Over-Apply When: + +- **Simple CRUD operations** with stable requirements +- **Small scripts or utilities** (< 100 lines) +- **Prototypes or POCs** for quick validation +- **Performance-critical code** where abstraction adds overhead +- **When it adds complexity** without clear benefit + +## Balancing Principles + +### When Principles Conflict + +**KISS vs DRY:** + +- Prefer KISS for simple cases +- Apply DRY only after Rule of Three +- Duplication is better than wrong abstraction + +**YAGNI vs Future-Proofing:** + +- Start with YAGNI +- Refactor when requirements actually arrive +- Don't over-engineer for hypothetical futures + +**SOLID vs KISS:** + +- Apply SOLID when complexity is justified +- Don't force patterns where they don't fit +- Simple problems deserve simple solutions + +**TDA vs Simple Data Objects:** + +- Use TDA for business logic +- Simple DTOs don't need behavior +- Value objects can be simple if immutable + +## Common Anti-Patterns + +### God Classes + +```typescript +// ❌ Classes doing too much (violates SRP) +export class UserService { + validateUser() {} + hashPassword() {} + sendEmail() {} + saveToDatabase() {} + generateReport() {} + processPayment() {} +} +``` + +### Premature Optimization + +```typescript +// ❌ Don't optimize before measuring +const cache = new Map(); +const lruCache = new LRUCache(1000); +const bloomFilter = new BloomFilter(); + +// ✅ Start simple, optimize when needed +const users = await repository.findAll(); +``` + +### Clever Code + +```typescript +// ❌ Clever but unreadable +const result = arr.reduce((a, b) => a + (b.active ? 1 : 0), 0); + +// ✅ Clear and boring +const activeCount = users.filter((user) => user.isActive).length; +``` + +### Magic Numbers + +```typescript +// ❌ Magic numbers +if (user.age > 18 && order.amount < 1000) { + // ... +} + +// ✅ Named constants +const MINIMUM_AGE = 18; +const MAXIMUM_ORDER_AMOUNT = 1000; + +if (user.age > MINIMUM_AGE && order.amount < MAXIMUM_ORDER_AMOUNT) { + // ... +} +``` + +## Validation Checklist + +Before finalizing code, verify: + +**SOLID Principles:** + +- [ ] Each class has a single, well-defined responsibility +- [ ] New features can be added without modifying existing code +- [ ] Subtypes are truly substitutable for their base types +- [ ] No class is forced to implement unused interface methods +- [ ] Dependencies point toward abstractions, not implementations + +**Clean Code Principles:** + +- [ ] Solution is as simple as possible (KISS) +- [ ] Only building what's needed now (YAGNI) +- [ ] Duplication abstracted after Rule of Three (DRY) +- [ ] Objects encapsulate behavior (TDA) +- [ ] Functions are < 20 lines +- [ ] Names are meaningful and reveal intention +- [ ] Code is self-documenting +- [ ] Early returns reduce nesting +- [ ] Single level of abstraction per function + +**Overall:** + +- [ ] Principles aren't creating unnecessary complexity +- [ ] Balance between design and pragmatism + +## Complete Example: Applying All Principles + +```typescript +// SRP + DIP: Each class has one responsibility, depends on abstractions +export interface Logger { + log(message: string): void; +} + +export interface UserRepository { + save(user: User): Promise; + findByEmail(email: string): Promise; +} + +export interface PasswordHasher { + hash(password: string): Promise; +} + +export interface EmailSender { + send(to: string, subject: string, body: string): Promise; +} + +// OCP: Open for extension (new implementations) +export class ConsoleLogger implements Logger { + log(message: string): void { + console.log(message); + } +} + +// ISP: Focused interfaces +// Each interface has a single, focused responsibility + +// KISS: Simple, clear implementation +export class CreateUserUseCase { + constructor( + private userRepository: UserRepository, + private passwordHasher: PasswordHasher, + private logger: Logger, + private emailSender: EmailSender + ) {} + + // KISS + Small Functions: < 20 lines, single responsibility + async execute(data: CreateUserDto): Promise { + this.logger.log("Creating new user"); + + // YAGNI: Only what's needed now + await this.validateEmail(data.email); + const user = await this.createUser(data); + await this.sendWelcomeEmail(user); + + this.logger.log("User created successfully"); + return user; + } + + // DRY: Extracted after Rule of Three + private async validateEmail(email: string): Promise { + const existing = await this.userRepository.findByEmail(email); + if (existing) { + throw new Error(`User with email ${email} already exists`); + } + } + + private async createUser(data: CreateUserDto): Promise { + const hashedPassword = await this.passwordHasher.hash(data.password); + const user = new User({ ...data, password: hashedPassword }); + await this.userRepository.save(user); + return user; + } + + private async sendWelcomeEmail(user: User): Promise { + await this.emailSender.send( + user.email, + "Welcome", + this.getWelcomeMessage(user.name) + ); + } + + // Self-documenting: Clear name, no comments needed + private getWelcomeMessage(name: string): string { + return `Welcome to our platform, ${name}!`; + } +} + +// LSP: Implementations are substitutable +export class BcryptPasswordHasher implements PasswordHasher { + async hash(password: string): Promise { + return bcrypt.hash(password, 10); + } +} + +export class ArgonPasswordHasher implements PasswordHasher { + async hash(password: string): Promise { + return argon2.hash(password); + } +} +``` + +## Integration with Architecture + +**SOLID + Clean Architecture:** + +- Domain entities use TDA (behavior with data) +- Use cases apply SRP (single responsibility) +- Repositories follow DIP (depend on interfaces) +- Infrastructure implements OCP (extend, don't modify) + +**Clean Code + KISS:** + +- Apply SOLID only when complexity is justified +- Don't create abstractions until you need them (YAGNI) +- Balance abstraction with code simplicity + +## Remember + +**Quality over dogma:** + +- Apply principles when they improve code, not just for the sake of it +- Context matters: Simple code doesn't need complex architecture +- Refactor gradually: Don't force patterns on existing code all at once + +**Communication over cleverness:** + +- Code is read 10x more than written +- Clear, boring code > clever, complex code +- Your future self will thank you + +**Pragmatism over perfection:** + +- SOLID principles make testing easier - use this as a guide +- Simple problems deserve simple solutions +- Test-driven: Let tests guide your design diff --git a/skills/error-handling-patterns/SKILL.md b/skills/error-handling-patterns/SKILL.md new file mode 100644 index 0000000..b1e0a59 --- /dev/null +++ b/skills/error-handling-patterns/SKILL.md @@ -0,0 +1,604 @@ +--- +name: error-handling-patterns +description: Error handling patterns including exceptions, Result pattern, validation strategies, retry logic, and circuit breakers. **ALWAYS use when implementing error handling in backend code, APIs, use cases, or validation logic.** Use proactively for robust error handling, recovery mechanisms, and failure scenarios. Examples - "handle errors", "Result pattern", "throw exception", "validate input", "error recovery", "retry logic", "circuit breaker", "exception hierarchy". +--- + +You are an expert in error handling patterns and strategies. You guide developers to implement robust, maintainable error handling that provides clear feedback and proper recovery mechanisms. + +**For complete backend implementation examples using these error handling patterns (Clean Architecture layers, DI Container, Use Cases, Repositories), see `backend-engineer` skill** + +## When to Engage + +You should proactively assist when: + +- Implementing error handling within bounded contexts +- Designing context-specific validation logic +- Creating context-specific exception types (no base classes) +- Implementing retry or recovery mechanisms per context +- User asks about error handling strategies +- Reviewing error handling without over-abstraction + +## Modular Monolith Error Handling + +### Context-Specific Errors (No Base Classes) + +```typescript +// ❌ BAD: Base error class creates coupling +export abstract class DomainError extends Error { + // Forces all contexts to use same error structure +} + +// ✅ GOOD: Each context has its own errors +// contexts/auth/domain/errors/auth-validation.error.ts +export class AuthValidationError extends Error { + constructor(message: string, public readonly field?: string) { + super(message); + this.name = "AuthValidationError"; + } +} + +// contexts/tax/domain/errors/tax-calculation.error.ts +export class TaxCalculationError extends Error { + constructor(message: string, public readonly ncmCode?: string) { + super(message); + this.name = "TaxCalculationError"; + } +} +``` + +### Error Handling Rules + +1. **Each context owns its errors** - No shared error classes +2. **Duplicate error structures** - Better than coupling through inheritance +3. **Context-specific metadata** - Each error has relevant context data +4. **Simple over clever** - Avoid complex error hierarchies + +## Core Principles + +### 1. Use Exceptions, Not Return Codes + +```typescript +// ✅ Good - Use exceptions with context +export class CreateUserUseCase { + async execute(dto: CreateUserDto): Promise { + if (!this.isValidEmail(dto.email)) { + throw new ValidationError("Invalid email format", { + email: dto.email, + field: "email", + }); + } + + try { + return await this.repository.save(user); + } catch (error) { + throw new DatabaseError("Failed to create user", { + originalError: error, + userId: user.id, + }); + } + } +} + +// ❌ Bad - Return codes +export class CreateUserUseCase { + async execute(dto: CreateUserDto): Promise<{ + success: boolean; + user?: User; + error?: string; + }> { + if (!this.isValidEmail(dto.email)) { + return { success: false, error: "Invalid email" }; + } + // Forces caller to check success flag everywhere + } +} +``` + +### 2. Never Return Null for Errors + +```typescript +// ✅ Good - Explicit optional with undefined +export class UserService { + async findById(id: string): Promise { + return this.repository.findById(id); + } + + // Or throw if must exist + async getUserById(id: string): Promise { + const user = await this.repository.findById(id); + if (!user) { + throw new NotFoundError(`User ${id} not found`); + } + return user; + } +} + +// ❌ Bad - Returning null loses error context +export class UserService { + async findById(id: string): Promise { + // Why null? Not found? Database error? Network error? + return null; + } +} +``` + +### 3. Provide Context with Exceptions + +```typescript +// ✅ Good - Rich error context +export class ValidationError extends Error { + constructor( + message: string, + public readonly context: Record + ) { + super(message); + this.name = "ValidationError"; + } +} + +throw new ValidationError("Invalid email format", { + email: dto.email, + field: "email", + rule: "email-format", + attemptedAt: new Date().toISOString(), +}); + +// ❌ Bad - No context +throw new Error("Invalid"); +``` + +## Exception Hierarchy + +### Custom Domain Exceptions + +```typescript +// Base domain exception +export abstract class DomainError extends Error { + constructor( + message: string, + public readonly code: string, + public readonly context?: Record + ) { + super(message); + this.name = this.constructor.name; + } +} + +// Specific domain exceptions +export class UserAlreadyExistsError extends DomainError { + constructor(email: string) { + super(`User with email ${email} already exists`, "USER_ALREADY_EXISTS", { + email, + }); + } +} + +export class InvalidPasswordError extends DomainError { + constructor(reason: string) { + super("Password does not meet requirements", "INVALID_PASSWORD", { + reason, + }); + } +} + +export class InsufficientPermissionsError extends DomainError { + constructor(userId: string, resource: string, action: string) { + super( + `User ${userId} cannot ${action} ${resource}`, + "INSUFFICIENT_PERMISSIONS", + { userId, resource, action } + ); + } +} +``` + +### Infrastructure Exceptions + +```typescript +export class DatabaseError extends Error { + constructor( + message: string, + public readonly originalError: unknown, + public readonly query?: string + ) { + super(message); + this.name = "DatabaseError"; + } +} + +export class ExternalServiceError extends Error { + constructor( + public readonly service: string, + message: string, + public readonly statusCode?: number, + public readonly originalError?: unknown + ) { + super(`${service}: ${message}`); + this.name = "ExternalServiceError"; + } +} +``` + +## Result Pattern + +For operations with expected failures: + +```typescript +export type Result = + | { success: true; value: T } + | { success: false; error: E }; + +export class UserService { + async findByEmail(email: string): Promise> { + const user = await this.repository.findByEmail(email); + + if (!user) { + return { + success: false, + error: new NotFoundError("User not found"), + }; + } + + return { success: true, value: user }; + } +} + +// Usage +const result = await userService.findByEmail("user@example.com"); + +if (!result.success) { + console.error("User not found:", result.error.message); + return; +} + +// TypeScript knows result.value is User here +const user = result.value; +``` + +### When to Use Result Pattern + +**Use Result for:** + +- Expected business failures (user not found, insufficient balance) +- Operations where failure is part of normal flow +- When caller needs to handle different failure types + +**Use Exceptions for:** + +- Unexpected errors (database connection lost, out of memory) +- Programming errors (invalid state, null pointer) +- Infrastructure failures + +## Validation Patterns + +### Input Validation at Boundaries + +```typescript +import { z } from "zod"; + +// ✅ Good - Validate at system boundaries +const CreateUserSchema = z.object({ + email: z.string().email("Invalid email format"), + password: z + .string() + .min(8, "Password must be at least 8 characters") + .regex(/[A-Z]/, "Password must contain uppercase letter") + .regex(/[0-9]/, "Password must contain number"), + name: z + .string() + .min(2, "Name must be at least 2 characters") + .max(100, "Name must be at most 100 characters"), + age: z + .number() + .int("Age must be an integer") + .min(18, "Must be at least 18 years old") + .optional(), +}); + +export type CreateUserDto = z.infer; + +// In Hono controller +import { zValidator } from "@hono/zod-validator"; + +app.post("/users", zValidator("json", CreateUserSchema), async (c) => { + const data = c.req.valid("json"); // Type-safe and validated + const user = await createUserUseCase.execute(data); + return c.json(user, 201); +}); +``` + +### Domain Validation + +```typescript +// ✅ Good - Validate in domain entities +export class Email { + private constructor(private readonly value: string) {} + + static create(value: string): Result { + if (!value) { + return { + success: false, + error: new ValidationError("Email is required", { field: "email" }), + }; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) { + return { + success: false, + error: new ValidationError("Invalid email format", { + email: value, + field: "email", + }), + }; + } + + return { success: true, value: new Email(value) }; + } + + toString(): string { + return this.value; + } +} + +// Usage +const emailResult = Email.create(dto.email); +if (!emailResult.success) { + throw emailResult.error; +} +const email = emailResult.value; +``` + +## Error Recovery Patterns + +### Retry Logic + +```typescript +export async function withRetry( + operation: () => Promise, + options: { + maxRetries: number; + delayMs: number; + shouldRetry?: (error: unknown) => boolean; + } +): Promise { + const { maxRetries, delayMs, shouldRetry = () => true } = options; + + let lastError: unknown; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await operation(); + } catch (error) { + lastError = error; + + if (attempt === maxRetries || !shouldRetry(error)) { + break; + } + + await new Promise((resolve) => + setTimeout(resolve, delayMs * (attempt + 1)) + ); + } + } + + throw lastError; +} + +// Usage +const user = await withRetry(() => userRepository.findById(id), { + maxRetries: 3, + delayMs: 1000, + shouldRetry: (error) => error instanceof DatabaseError, +}); +``` + +### Circuit Breaker + +```typescript +export class CircuitBreaker { + private failureCount = 0; + private lastFailureTime?: number; + private state: "CLOSED" | "OPEN" | "HALF_OPEN" = "CLOSED"; + + constructor( + private readonly failureThreshold: number, + private readonly resetTimeoutMs: number + ) {} + + async execute(operation: () => Promise): Promise { + if (this.state === "OPEN") { + if (Date.now() - (this.lastFailureTime || 0) > this.resetTimeoutMs) { + this.state = "HALF_OPEN"; + } else { + throw new Error("Circuit breaker is OPEN"); + } + } + + try { + const result = await operation(); + this.onSuccess(); + return result; + } catch (error) { + this.onFailure(); + throw error; + } + } + + private onSuccess(): void { + this.failureCount = 0; + this.state = "CLOSED"; + } + + private onFailure(): void { + this.failureCount++; + this.lastFailureTime = Date.now(); + + if (this.failureCount >= this.failureThreshold) { + this.state = "OPEN"; + } + } +} + +// Usage +const breaker = new CircuitBreaker(5, 60000); // 5 failures, 60s timeout + +const data = await breaker.execute(() => externalService.fetchData()); +``` + +### Fallback Values + +```typescript +export class ConfigService { + async get(key: string, fallback: T): Promise { + try { + const value = await this.redis.get(key); + return value ? JSON.parse(value) : fallback; + } catch (error) { + console.error(`Failed to get config ${key}:`, error); + return fallback; + } + } +} + +// Usage +const maxRetries = await configService.get("maxRetries", 3); +``` + +## Error Logging + +### Structured Logging + +```typescript +export interface LogContext { + userId?: string; + requestId?: string; + operation?: string; + [key: string]: unknown; +} + +export class Logger { + error(message: string, error: Error, context?: LogContext): void { + console.error({ + level: "error", + message, + error: { + name: error.name, + message: error.message, + stack: error.stack, + }, + context, + timestamp: new Date().toISOString(), + }); + } + + warn(message: string, context?: LogContext): void { + console.warn({ + level: "warn", + message, + context, + timestamp: new Date().toISOString(), + }); + } +} + +// Usage +try { + await userRepository.save(user); +} catch (error) { + logger.error("Failed to save user", error as Error, { + userId: user.id, + operation: "createUser", + requestId: context.requestId, + }); + throw new DatabaseError("Failed to save user", error); +} +``` + +## HTTP Error Handling (Hono) + +```typescript +import { Hono } from "hono"; +import type { Context } from "hono"; + +const app = new Hono(); + +// Global error handler +app.onError((err, c) => { + logger.error("Unhandled error", err, { + path: c.req.path, + method: c.req.method, + }); + + if (err instanceof ValidationError) { + return c.json( + { + error: "Validation failed", + message: err.message, + context: err.context, + }, + 400 + ); + } + + if (err instanceof NotFoundError) { + return c.json( + { + error: "Resource not found", + message: err.message, + }, + 404 + ); + } + + if (err instanceof DomainError) { + return c.json( + { + error: err.code, + message: err.message, + context: err.context, + }, + 400 + ); + } + + // Unknown error - don't leak details + return c.json( + { + error: "Internal server error", + message: "An unexpected error occurred", + }, + 500 + ); +}); +``` + +## Best Practices + +### Do: + +- ✅ Throw exceptions for exceptional situations +- ✅ Use Result pattern for expected failures +- ✅ Provide rich context in errors +- ✅ Validate at system boundaries +- ✅ Log errors with correlation IDs +- ✅ Implement retry for transient failures +- ✅ Use circuit breakers for external services +- ✅ Create domain-specific exception types + +### Don't: + +- ❌ Swallow exceptions silently +- ❌ Return null for errors +- ❌ Use exceptions for control flow +- ❌ Leak implementation details in error messages +- ❌ Throw generic Error instances +- ❌ Catch and re-throw without adding context +- ❌ Ignore errors in async operations + +## Remember + +- **Fail fast, fail loudly** - Don't hide errors +- **Context is king** - Provide rich error information +- **Recovery is better than failure** - Implement fallbacks when possible +- **Log for debugging** - Future you will thank you diff --git a/skills/frontend-engineer/SKILL.md b/skills/frontend-engineer/SKILL.md new file mode 100644 index 0000000..cf84bf5 --- /dev/null +++ b/skills/frontend-engineer/SKILL.md @@ -0,0 +1,1072 @@ +--- +name: frontend-engineer +description: Expert frontend engineering with simplified pragmatic architecture, React 19, TanStack ecosystem, and Zustand state management. **ALWAYS use when implementing ANY frontend features.** Use when setting up project structure, creating pages and state management, designing gateway injection patterns, setting up HTTP communication and routing, organizing feature modules, or optimizing performance. **ALWAYS use when implementing Gateway Pattern (Interface + HTTP + Fake), Context API injection, Zustand stores, TanStack Router, or feature-based architecture.** +--- + +# Frontend Engineer Skill + +**Purpose**: Expert frontend engineering with simplified pragmatic architecture, React 19, TanStack ecosystem, and Zustand state management. Provides implementation examples for building testable, maintainable, scalable frontend applications. + +**When to Use**: + +- Implementing frontend features and components +- Setting up project structure +- Creating pages and state management +- Designing gateway injection patterns +- Setting up HTTP communication and routing +- Organizing feature modules +- Performance optimization and code splitting + +**NOTE**: For UI component design, Tailwind styling, shadcn/ui setup, responsive layouts, and accessibility implementation, defer to the `ui-designer` skill. This skill focuses on architecture, state management, and business logic orchestration. + +--- + +## Documentation Lookup (MANDATORY) + +**ALWAYS use MCP servers for up-to-date documentation:** + +- **Context7 MCP**: Use for comprehensive library documentation, API reference, import statements, and version-specific patterns + + - When user asks about TanStack Router, Query, Form, Table APIs + - For React 19 features and patterns + - For Vite configuration and build setup + - For Zustand API and patterns + - To verify correct import paths, hooks usage, and API patterns + +- **Perplexity MCP**: Use for architectural research, design patterns, and best practices + - When researching pragmatic frontend architectures + - For state management strategies and trade-offs + - For performance optimization techniques + - For folder structure and code organization patterns + - For troubleshooting complex architectural issues + +**Examples of when to use MCP:** + +- "How to setup TanStack Router file-based routing?" → Use Context7 MCP for TanStack Router docs +- "What are React 19 use() hook patterns?" → Use Context7 MCP for React docs +- "How to use Zustand with TypeScript?" → Use Context7 MCP for Zustand docs +- "Best practices for feature-based architecture in React?" → Use Perplexity MCP for research +- "How to configure Vite with React 19?" → Use Context7 MCP for Vite docs + +--- + +## Tech Stack + +**For complete frontend tech stack details, see "Tech Stack > Frontend" section in CLAUDE.md** + +**Quick Architecture Reference:** + +This skill focuses on: + +- Feature-based architecture (NOT Clean Architecture layers) +- Gateway Pattern (Interface + HTTP + Fake) +- Context API injection for testability +- Zustand for global client state +- TanStack Query for server state + +### Testing: + +- **Unit**: Vitest + React Testing Library +- **E2E**: Playwright + +→ See `project-standards` skill for complete tech stack +→ See `ui-designer` skill for UI/UX specific technologies +→ See `docs/plans/2025-10-24-simplified-frontend-architecture-design.md` for architecture details + +--- + +## Architecture Overview + +Frontend follows a **simplified, pragmatic, feature-based architecture**. + +**Core Principles:** + +1. **Feature-based organization** - Code grouped by business functionality +2. **Pages as use cases** - Pages orchestrate business logic +3. **Externalized state** - Zustand stores are framework-agnostic, 100% testable +4. **Gateway injection** - Gateways injected via Context API for isolated testing +5. **YAGNI rigorously** - No unnecessary layers or abstractions + +**Design Philosophy**: Simplicity and pragmatism over architectural purity. Remove Clean Architecture complexity (domain/application/infrastructure layers) in favor of direct, maintainable code. + +### Recommended Structure + +``` +apps/web/src/ +├── app/ # Application setup +│ ├── config/ +│ │ ├── env.ts # Environment variables +│ │ └── index.ts +│ ├── providers/ +│ │ ├── gateway-provider.tsx # Gateway injection (Context API) +│ │ ├── query-provider.tsx # TanStack Query setup +│ │ ├── theme-provider.tsx # Theme provider (shadcn/ui) +│ │ └── index.ts +│ ├── router.tsx # TanStack Router configuration +│ └── main.tsx # Application entry point +│ +├── features/ # Feature modules (self-contained) +│ ├── auth/ +│ │ ├── components/ # Pure UI components +│ │ │ ├── login-form.tsx +│ │ │ └── index.ts +│ │ ├── pages/ # Use cases - orchestrate logic +│ │ │ ├── login-page.tsx +│ │ │ └── index.ts +│ │ ├── stores/ # Zustand stores - testable entities +│ │ │ ├── auth-store.ts +│ │ │ └── index.ts +│ │ ├── gateways/ # External resource abstractions +│ │ │ ├── auth-gateway.ts # Interface + HTTP implementation +│ │ │ ├── auth-gateway.fake.ts # Fake for unit tests +│ │ │ └── index.ts +│ │ ├── hooks/ # Custom hooks (optional) +│ │ │ └── index.ts +│ │ ├── types/ # TypeScript types +│ │ │ ├── user.ts +│ │ │ └── index.ts +│ │ └── index.ts # Barrel file - public API +│ │ +│ ├── dashboard/ +│ └── profile/ +│ +├── shared/ # Shared code across features +│ ├── services/ # Global services +│ │ ├── http-api.ts # HTTP client base (Axios wrapper) +│ │ ├── storage.ts # LocalStorage/Cookie abstraction +│ │ └── index.ts +│ ├── components/ +│ │ ├── ui/ # shadcn/ui components +│ │ └── layout/ # Shared layouts +│ ├── hooks/ # Global utility hooks +│ ├── lib/ # Utilities and helpers +│ │ ├── validators.ts # Zod schemas (common) +│ │ ├── formatters.ts +│ │ └── index.ts +│ └── types/ # Global types +│ +├── routes/ # TanStack Router routes +│ ├── __root.tsx +│ ├── index.tsx +│ └── auth/ +│ └── login.tsx +│ +└── index.css +``` + +**Benefits**: + +- ✅ Feature isolation - delete folder, remove feature +- ✅ No unnecessary layers - direct, maintainable code +- ✅ Testable business logic - Zustand stores are pure JS/TS +- ✅ Isolated page testing - inject fake gateways +- ✅ Clear separation - pages (orchestration), components (UI), stores (state) + +--- + +## Layer Responsibilities (MANDATORY) + +### 1. Shared Services Layer + +**Purpose**: Reusable infrastructure used across features. + +**Example - HTTP Client**: + +```typescript +// shared/services/http-api.ts +import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios"; + +export class HttpApi { + private client: AxiosInstance; + + constructor(baseURL: string) { + this.client = axios.create({ + baseURL, + timeout: 10000, + headers: { "Content-Type": "application/json" }, + }); + + // Auto-inject auth token + this.client.interceptors.request.use((config) => { + const token = localStorage.getItem("auth_token"); + if (token) config.headers.Authorization = `Bearer ${token}`; + return config; + }); + + // Handle 401 globally + this.client.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem("auth_token"); + window.location.href = "/auth/login"; + } + return Promise.reject(error); + } + ); + } + + async get(url: string, config?: AxiosRequestConfig) { + const response = await this.client.get(url, config); + return response.data; + } + + async post(url: string, data?: unknown, config?: AxiosRequestConfig) { + const response = await this.client.post(url, data, config); + return response.data; + } + + async put(url: string, data?: unknown, config?: AxiosRequestConfig) { + const response = await this.client.put(url, data, config); + return response.data; + } + + async delete(url: string, config?: AxiosRequestConfig) { + const response = await this.client.delete(url, config); + return response.data; + } +} + +// Singleton instance +export const httpApi = new HttpApi(import.meta.env.VITE_API_URL); +``` + +--- + +### 2. Gateway Layer + +**Purpose**: Abstract external resource access. Enable testing with fakes. + +**Example - Auth Gateway**: + +```typescript +// features/auth/gateways/auth-gateway.ts +import { httpApi } from "@/shared/services/http-api"; +import type { User } from "../types/user"; + +export interface LoginRequest { + email: string; + password: string; +} + +export interface LoginResponse { + user: User; + token: string; +} + +// Gateway interface (contract) +export interface AuthGateway { + login(request: LoginRequest): Promise; + logout(): Promise; + getCurrentUser(): Promise; +} + +// Real implementation (HTTP) +export class AuthHttpGateway implements AuthGateway { + async login(request: LoginRequest): Promise { + return httpApi.post("/auth/login", request); + } + + async logout(): Promise { + await httpApi.post("/auth/logout"); + } + + async getCurrentUser(): Promise { + return httpApi.get("/auth/me"); + } +} + +// Fake implementation for unit tests +export class AuthFakeGateway implements AuthGateway { + private shouldFail = false; + + setShouldFail(value: boolean) { + this.shouldFail = value; + } + + async login(request: LoginRequest): Promise { + await new Promise((resolve) => setTimeout(resolve, 100)); + + if (this.shouldFail) { + throw new Error("Invalid credentials"); + } + + if ( + request.email === "test@example.com" && + request.password === "password123" + ) { + return { + user: { + id: "1", + name: "Test User", + email: "test@example.com", + role: "user", + }, + token: "fake-token-123", + }; + } + + throw new Error("Invalid credentials"); + } + + async logout(): Promise { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + + async getCurrentUser(): Promise { + if (this.shouldFail) throw new Error("Unauthorized"); + return { + id: "1", + name: "Test User", + email: "test@example.com", + role: "user", + }; + } +} +``` + +--- + +### 3. Gateway Injection (Context API) + +**Purpose**: Provide gateways to components. Allow override in tests. + +**Example**: + +```typescript +// app/providers/gateway-provider.tsx +import { createContext, useContext, type ReactNode } from "react"; +import { + AuthGateway, + AuthHttpGateway, +} from "@/features/auth/gateways/auth-gateway"; + +interface Gateways { + authGateway: AuthGateway; + // Add more gateways as needed +} + +const GatewayContext = createContext(null); + +interface GatewayProviderProps { + children: ReactNode; + gateways?: Partial; // Allow override for tests +} + +export function GatewayProvider({ children, gateways }: GatewayProviderProps) { + const defaultGateways: Gateways = { + authGateway: new AuthHttpGateway(), + }; + + const value = { ...defaultGateways, ...gateways }; + + return ( + {children} + ); +} + +export function useGateways() { + const context = useContext(GatewayContext); + if (!context) { + throw new Error("useGateways must be used within GatewayProvider"); + } + return context; +} +``` + +--- + +### 4. Store Layer (Zustand) + +**Purpose**: Testable business logic, framework-agnostic state management. + +**Example**: + +```typescript +// features/auth/stores/auth-store.ts +import { create } from "zustand"; +import type { User } from "../types/user"; + +interface AuthState { + user: User | null; + isAuthenticated: boolean; + isLoading: boolean; + error: string | null; + + setUser: (user: User | null) => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; + reset: () => void; +} + +export const useAuthStore = create((set) => ({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + + setUser: (user) => set({ user, isAuthenticated: user !== null }), + setLoading: (isLoading) => set({ isLoading }), + setError: (error) => set({ error }), + reset: () => + set({ user: null, isAuthenticated: false, isLoading: false, error: null }), +})); +``` + +**Why Zustand:** + +- ✅ Minimal boilerplate +- ✅ Excellent TypeScript support +- ✅ Framework-agnostic (100% testable without React) +- ✅ Large community and ecosystem +- ✅ Superior DX compared to alternatives + +--- + +### 5. Page Layer (Use Cases) + +**Purpose**: Orchestrate business logic by coordinating gateways, stores, and UI components. + +**Example**: + +```typescript +// features/auth/pages/login-page.tsx +import { useState } from "react"; +import { useNavigate } from "@tanstack/react-router"; +import { useGateways } from "@/app/providers/gateway-provider"; +import { useAuthStore } from "../stores/auth-store"; +import { LoginForm } from "../components/login-form"; + +export function LoginPage() { + const navigate = useNavigate(); + const { authGateway } = useGateways(); // Injected gateway + const { setUser, setLoading, setError, isLoading, error } = useAuthStore(); + + const [formError, setFormError] = useState(null); + + const handleLogin = async (email: string, password: string) => { + setLoading(true); + setError(null); + setFormError(null); + + try { + const { user, token } = await authGateway.login({ email, password }); + + localStorage.setItem("auth_token", token); + setUser(user); + + navigate({ to: "/dashboard" }); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Login failed"; + setError(errorMessage); + setFormError(errorMessage); + } finally { + setLoading(false); + } + }; + + return ( +
+ +
+ ); +} +``` + +--- + +### 6. Component Layer + +**Purpose**: Pure UI components that receive props and emit events. + +**Example**: + +```typescript +// features/auth/components/login-form.tsx +import { useState } from "react"; +import { Button } from "@/shared/components/ui/button"; +import { Input } from "@/shared/components/ui/input"; + +interface LoginFormProps { + onSubmit: (email: string, password: string) => void; + isLoading?: boolean; + error?: string | null; +} + +export function LoginForm({ + onSubmit, + isLoading = false, + error, +}: LoginFormProps) { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(email, password); + }; + + return ( +
+
+ setEmail(e.target.value)} + disabled={isLoading} + aria-label="Email" + /> +
+ +
+ setPassword(e.target.value)} + disabled={isLoading} + aria-label="Password" + /> +
+ + {error &&

{error}

} + + +
+ ); +} +``` + +--- + +## State Management Strategy (MANDATORY) + +**Use the RIGHT tool for each state type:** + +### 1. Global Client State → Zustand + +For application-wide client state (auth, theme, preferences): + +```typescript +// shared/stores/theme-store.ts +import { create } from "zustand"; + +export type Theme = "light" | "dark" | "system"; + +interface ThemeState { + theme: Theme; + setTheme: (theme: Theme) => void; +} + +export const useThemeStore = create((set) => ({ + theme: "system", + setTheme: (theme) => set({ theme }), +})); +``` + +### 2. Server State → TanStack Query + +For data from backend APIs with caching, revalidation, and synchronization: + +```typescript +// features/users/hooks/use-users-query.ts +import { useQuery } from "@tanstack/react-query"; +import { useGateways } from "@/app/providers/gateway-provider"; + +export function useUsersQuery() { + const { userGateway } = useGateways(); + + return useQuery({ + queryKey: ["users"], + queryFn: () => userGateway.getUsers(), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +} +``` + +### 3. Form State → TanStack Form + +For form management with validation: + +```typescript +import { useForm } from "@tanstack/react-form"; +import { zodValidator } from "@tanstack/zod-form-adapter"; +import { z } from "zod"; + +const userSchema = z.object({ + name: z.string().min(2), + email: z.string().email(), +}); + +export function UserForm() { + const form = useForm({ + defaultValues: { name: "", email: "" }, + validatorAdapter: zodValidator(), + validators: { onChange: userSchema }, + onSubmit: async ({ value }) => console.log(value), + }); + + // ... render form +} +``` + +### 4. URL State → TanStack Router + +For URL parameters and search params: + +```typescript +// routes/users/$userId.tsx +import { createFileRoute } from "@tanstack/react-router"; +import { z } from "zod"; + +const userSearchSchema = z.object({ + tab: z.enum(["profile", "settings"]).optional(), +}); + +export const Route = createFileRoute("/users/$userId")({ + validateSearch: userSearchSchema, + component: UserDetail, +}); + +function UserDetail() { + const { userId } = Route.useParams(); + const { tab = "profile" } = Route.useSearch(); + + return ( +
+ User {userId} - Tab: {tab} +
+ ); +} +``` + +### 5. Local Component State → useState/useReducer + +For component-specific state: + +```typescript +export function Counter() { + const [count, setCount] = useState(0); + + return ( +
+

Count: {count}

+ +
+ ); +} +``` + +--- + +## Testing Strategy (MANDATORY) + +### Test Pyramid + +``` + ┌─────────────┐ + ╱ E2E Tests ╲ Few - Critical flows + ╱ (Playwright) ╲ + ╱─────────────────────╲ + ╱ Integration Tests ╲ Some - Pages with fakes + ╱ (Pages + Stores) ╲ + ╱─────────────────────────╲ + ╱ Unit Tests ╲ Many - Stores, utils +╱ (Stores, Utils, Comps) ╲ +───────────────────────────── +``` + +### Store Tests (Unit - High Priority) + +Zustand stores are 100% testable without React: + +```typescript +// features/auth/stores/auth-store.test.ts +import { describe, it, expect, beforeEach } from "bun:test"; +import { useAuthStore } from "./auth-store"; + +describe("AuthStore", () => { + beforeEach(() => { + useAuthStore.getState().reset(); + }); + + it("should start with empty state", () => { + const state = useAuthStore.getState(); + expect(state.user).toBeNull(); + expect(state.isAuthenticated).toBe(false); + }); + + it("should update user and isAuthenticated when setting user", () => { + const user = { + id: "1", + name: "Test", + email: "test@example.com", + role: "user", + }; + + useAuthStore.getState().setUser(user); + + expect(useAuthStore.getState().user).toEqual(user); + expect(useAuthStore.getState().isAuthenticated).toBe(true); + }); +}); +``` + +### Page Tests (Integration with Fake Gateways) + +Test orchestration logic with injected fakes: + +```typescript +// features/auth/pages/login-page.test.tsx +import { describe, it, expect, beforeEach } from "bun:test"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { GatewayProvider } from "@/app/providers/gateway-provider"; +import { AuthFakeGateway } from "../gateways/auth-gateway"; +import { LoginPage } from "./login-page"; +import { useAuthStore } from "../stores/auth-store"; + +describe("LoginPage", () => { + let fakeAuthGateway: AuthFakeGateway; + + beforeEach(() => { + fakeAuthGateway = new AuthFakeGateway(); + useAuthStore.getState().reset(); + }); + + it("should login successfully and update store", async () => { + const user = userEvent.setup(); + + render( + + + + ); + + await user.type(screen.getByLabelText(/email/i), "test@example.com"); + await user.type(screen.getByLabelText(/password/i), "password123"); + await user.click(screen.getByRole("button", { name: /login/i })); + + await waitFor(() => { + expect(useAuthStore.getState().isAuthenticated).toBe(true); + }); + }); + + it("should show error when login fails", async () => { + const user = userEvent.setup(); + fakeAuthGateway.setShouldFail(true); + + render( + + + + ); + + await user.type(screen.getByLabelText(/email/i), "wrong@example.com"); + await user.type(screen.getByLabelText(/password/i), "wrong"); + await user.click(screen.getByRole("button", { name: /login/i })); + + await waitFor(() => { + expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument(); + }); + }); +}); +``` + +### Component Tests (Unit - Pure UI) + +```typescript +// features/auth/components/login-form.test.tsx +import { describe, it, expect, mock } from "bun:test"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { LoginForm } from "./login-form"; + +describe("LoginForm", () => { + it("should render email and password fields", () => { + render(); + + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); + }); + + it("should call onSubmit with correct values", async () => { + const user = userEvent.setup(); + const onSubmit = mock(); + + render(); + + await user.type(screen.getByLabelText(/email/i), "test@example.com"); + await user.type(screen.getByLabelText(/password/i), "password123"); + await user.click(screen.getByRole("button", { name: /login/i })); + + expect(onSubmit).toHaveBeenCalledWith("test@example.com", "password123"); + }); +}); +``` + +### E2E Tests (Playwright - Critical Flows) + +```typescript +// e2e/auth/login.spec.ts +import { test, expect } from "@playwright/test"; + +test.describe("Login Flow", () => { + test("should login and redirect to dashboard", async ({ page }) => { + await page.goto("/auth/login"); + + await page.fill('input[name="email"]', "test@example.com"); + await page.fill('input[name="password"]', "password123"); + await page.click('button:has-text("Login")'); + + await expect(page).toHaveURL("/dashboard"); + await expect(page.locator("text=Welcome, Test User")).toBeVisible(); + }); +}); +``` + +--- + +## Naming Conventions (MANDATORY) + +### Files and Folders + +- **Files**: `kebab-case` with suffixes +- **Folders**: `kebab-case` + +``` +features/auth/ +├── components/ +│ ├── login-form.tsx # Component: kebab-case +│ └── index.ts +├── pages/ +│ ├── login-page.tsx # Suffix: -page.tsx +│ └── index.ts +├── stores/ +│ ├── auth-store.ts # Suffix: -store.ts +│ └── index.ts +├── gateways/ +│ ├── auth-gateway.ts # Suffix: -gateway.ts +│ ├── auth-gateway.fake.ts # Suffix: -gateway.fake.ts +│ └── index.ts +``` + +### Code Elements + +**Components**: + +```typescript +// ✅ Good +export function LoginForm({ onSubmit }: LoginFormProps) {} + +// ❌ Avoid +export const loginForm = () => {}; // No arrow function exports +export function Form() {} // Too generic +``` + +**Pages**: + +```typescript +// ✅ Good - "Page" suffix +export function LoginPage() {} + +// ❌ Avoid +export function Login() {} // Confuses with component +``` + +**Stores**: + +```typescript +// ✅ Good - "use" + name + "Store" +export const useAuthStore = create(...); + +// ❌ Avoid +export const authStore = create(...); // Missing "use" prefix +``` + +**Gateways**: + +```typescript +// ✅ Good - No "I" prefix +export interface AuthGateway {} +export class AuthHttpGateway implements AuthGateway {} +export class AuthFakeGateway implements AuthGateway {} + +// ❌ Avoid +export interface IAuthGateway {} // No "I" prefix +``` + +--- + +## TanStack Router Patterns (MANDATORY) + +### File-Based Routing + +```typescript +// routes/__root.tsx +import { createRootRoute, Outlet } from "@tanstack/react-router"; + +export const Route = createRootRoute({ + component: () => ( +
+ + +
+ ), +}); + +// routes/index.tsx +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ + component: HomePage, +}); + +function HomePage() { + return

Home

; +} +``` + +### Type-Safe Navigation + +```typescript +import { Link, useNavigate } from "@tanstack/react-router"; + +export function Navigation() { + const navigate = useNavigate(); + + return ( +
+ + View User + + + +
+ ); +} +``` + +--- + +## Component Organization Principles (MANDATORY) + +### 1. Keep Components Small (< 150 lines) + +```typescript +// ✅ Good - Extract into smaller components +export function UserDashboard() { + return ( +
+ + + +
+ ); +} +``` + +### 2. Extract Logic into Hooks (< 20 lines in component) + +```typescript +// ✅ Good +export function UserList() { + const { users, loading, error } = useUsers(); + + if (loading) return ; + if (error) return ; + + return
{/* Render users */}
; +} +``` + +### 3. One Component Per File + +```typescript +// ✅ Good - Separate files +// user-card.tsx +export function UserCard() {} + +// user-avatar.tsx +export function UserAvatar() {} +``` + +--- + +## Critical Rules + +**NEVER:** + +- Use Clean Architecture layers (domain/application/infrastructure) +- Import stores in components (pass data via props) +- Call HTTP directly from components (use gateways) +- Use `any` type (use `unknown` with type guards) +- Skip TypeScript types +- Use npm, pnpm, yarn (use Bun) +- Create components > 150 lines +- Keep logic > 20 lines in components + +**ALWAYS:** + +- Organize by features (feature-based structure) +- Pages orchestrate logic (use gateways + stores) +- Externalize state to Zustand stores +- Inject gateways via Context API +- Write tests for stores (unit) and pages (integration with fakes) +- Use functional components with TypeScript +- One component per file +- Extract logic into custom hooks +- Use Bun for all package management +- Run `bun run craft` after creating/moving files + +--- + +## Deliverables + +When helping users, provide: + +1. **Feature Folder Structure**: Organized with pages/, components/, stores/, gateways/ +2. **Gateway Definitions**: Interface + HTTP implementation + Fake implementation +3. **Gateway Provider**: Context API setup for dependency injection +4. **Zustand Stores**: Framework-agnostic state management with actions +5. **Pages**: Orchestration logic using gateways and stores +6. **Components**: Pure UI components with props +7. **Router Configuration**: TanStack Router setup with type safety +8. **Test Examples**: Store tests, page tests with fakes, component tests, E2E +9. **Configuration Files**: Vite, TypeScript, TanStack configurations + +--- + +## Summary + +Frontend architecture focuses on: + +1. **Simplicity**: No unnecessary layers, direct code, pragmatic patterns +2. **Testability**: Zustand stores (pure), pages with fake gateways, isolated components +3. **Maintainability**: Feature-based structure, clear responsibilities, consistent patterns +4. **Type Safety**: Strong typing, Zod validation, TanStack Router +5. **Performance**: Code splitting, lazy loading, optimistic updates +6. **User Experience**: Error handling, loading states, toast notifications + +**Remember**: Good architecture makes change easy. Build systems that are simple, testable, and maintainable. Avoid over-engineering. Focus on delivering value. + +--- + +**Reference**: + +- Architecture design: `docs/plans/2025-10-24-simplified-frontend-architecture-design.md` +- Project standards: `project-standards` skill +- UI/UX: `ui-designer` skill diff --git a/skills/naming-conventions/SKILL.md b/skills/naming-conventions/SKILL.md new file mode 100644 index 0000000..614733a --- /dev/null +++ b/skills/naming-conventions/SKILL.md @@ -0,0 +1,553 @@ +--- +name: naming-conventions +description: Expert in naming conventions for files, directories, classes, functions, and variables. **ALWAYS use when creating ANY files, folders, classes, functions, or variables, OR when renaming any code elements.** Use proactively to ensure consistent, readable naming across the codebase. Examples - "create new component", "create file", "create folder", "name this function", "rename function", "rename file", "rename class", "refactor variable names", "review naming conventions". +--- + +You are an expert in naming conventions and code organization. You ensure consistent, readable, and maintainable naming across the entire codebase following industry best practices. + +## When to Engage + +You should proactively assist when users: + +- Create new files, folders, or code structures within contexts +- Name context-specific variables, functions, classes, or interfaces +- Review code for naming consistency across bounded contexts +- Refactor existing code to follow context isolation +- Ask about naming patterns for Modular Monolith + +## Modular Monolith Naming Conventions + +### Bounded Context Structure + +``` +apps/nexus/src/ +├── contexts/ # Always plural +│ ├── auth/ # Context name: singular, kebab-case +│ │ ├── domain/ # Clean Architecture layers +│ │ ├── application/ +│ │ └── infrastructure/ +│ │ +│ ├── tax/ # Short, descriptive context names +│ ├── bi/ # Abbreviations OK if clear +│ └── production/ +│ +└── shared/ # Minimal shared kernel + └── domain/ + └── value-objects/ # ONLY uuidv7 and timestamp +``` + +### Context-Specific Naming + +```typescript +// ✅ GOOD: Context prefix in class names when needed for clarity +export class AuthValidationError extends Error {} +export class TaxCalculationError extends Error {} + +// ✅ GOOD: No prefix when context is clear from import +import { User } from "@auth/domain/entities/user.entity"; +import { NcmCode } from "@tax/domain/value-objects/ncm-code.value-object"; + +// ❌ BAD: Generic names that require base classes +export abstract class BaseEntity {} // NO! +export abstract class BaseError {} // NO! +``` + +## File Naming Conventions + +### Pattern: `kebab-case` with descriptive suffixes + +**Domain Layer**: + +``` +user.entity.ts # Domain entities +email.value-object.ts # Value objects +user-id.value-object.ts # Composite value objects +create-user.use-case.ts # Use cases/application services +user.aggregate.ts # Aggregate roots +``` + +**Infrastructure Layer**: + +``` +postgres-user.repository.ts # Repository implementations +redis-cache.service.ts # External service implementations +user.repository.ts # Repository interfaces +payment.gateway.ts # Gateway interfaces +``` + +**Application Layer**: + +``` +create-user.dto.ts # Data Transfer Objects +user-response.dto.ts # Response DTOs +user.mapper.ts # Entity-DTO mappers +``` + +**Base/Abstract Classes**: + +``` +entity.base.ts # Base entity class +value-object.base.ts # Base value object +repository.base.ts # Base repository interface +``` + +**Controllers & Routes**: + +``` +user.controller.ts # HTTP controllers +auth.routes.ts # Route definitions +user.middleware.ts # Middleware functions +``` + +**Tests**: + +``` +user.entity.test.ts # Unit tests +create-user.use-case.test.ts # Use case tests +user.e2e.test.ts # E2E tests +``` + +### Checklist for Files: + +- [ ] Uses `kebab-case` +- [ ] Has descriptive suffix (`.entity.ts`, `.repository.ts`, etc.) +- [ ] Suffix matches file content/purpose +- [ ] Name is clear and searchable + +## Directory Naming Conventions + +### Pattern: Use **plural** for collections, **singular** for feature modules + +**Correct Structure**: + +``` +src/ +├── domain/ +│ ├── entities/ # ✅ Plural - collection of entities +│ ├── value-objects/ # ✅ Plural - collection of VOs +│ ├── aggregates/ # ✅ Plural - collection of aggregates +│ └── events/ # ✅ Plural - collection of events +├── application/ +│ ├── use-cases/ # ✅ Plural - collection of use cases +│ └── dtos/ # ✅ Plural - collection of DTOs +├── infrastructure/ +│ ├── repositories/ # ✅ Plural - collection of repos +│ ├── services/ # ✅ Plural - collection of services +│ └── gateways/ # ✅ Plural - collection of gateways +├── modules/ +│ ├── auth/ # ✅ Singular - feature module +│ ├── user/ # ✅ Singular - feature module +│ └── payment/ # ✅ Singular - feature module +``` + +**Why This Pattern?**: + +- **Plural directories** = Collections of similar items (like a folder of files) +- **Singular modules** = Single feature/bounded context (like a package) + +### Checklist for Directories: + +- [ ] Collection directories are plural (`entities/`, `repositories/`) +- [ ] Feature modules are singular (`auth/`, `user/`) +- [ ] Uses `kebab-case` for multi-word names +- [ ] Structure reflects architecture layers + +## Code Naming Conventions + +### Classes & Interfaces: `PascalCase` + +```typescript +// ✅ Good +export class UserEntity {} +export class CreateUserUseCase {} +export interface UserRepository {} +export type UserId = string; +export enum UserRole {} + +// ❌ Bad +export class userEntity {} // Should be PascalCase +export class create_user_usecase {} // Should be PascalCase +export interface IUserRepository {} // No 'I' prefix +``` + +**Rules**: + +- Use nouns for classes and types +- Use descriptive names for interfaces (no `I` prefix) +- Enums should be singular (`UserRole`, not `UserRoles`) + +### Functions & Variables: `camelCase` + +```typescript +// ✅ Good +const userName = "John"; +const isActive = true; +const hasVerifiedEmail = false; + +function createUser(data: CreateUserDto): User { + // Implementation +} + +async function fetchUserById(id: string): Promise { + // Implementation +} + +// ❌ Bad +const UserName = "John"; // Should be camelCase +const is_active = true; // Should be camelCase +function CreateUser() {} // Should be camelCase +async function fetch_user() {} // Should be camelCase +``` + +**Rules**: + +- Use verbs for function names (`create`, `fetch`, `update`, `delete`) +- Boolean variables start with `is`, `has`, `can`, `should` +- Async functions should indicate they're async in name when helpful + +### Constants: `UPPER_SNAKE_CASE` + +```typescript +// ✅ Good +export const MAX_RETRY_ATTEMPTS = 3; +export const DEFAULT_TIMEOUT_MS = 5000; +export const API_BASE_URL = "https://api.example.com"; +export const DATABASE_CONNECTION_POOL_SIZE = 10; + +// ❌ Bad +export const maxRetryAttempts = 3; // Should be UPPER_SNAKE_CASE +export const defaultTimeout = 5000; // Should be UPPER_SNAKE_CASE +``` + +**Rules**: + +- Only for true constants (compile-time or startup values) +- Include units in name when relevant (`_MS`, `_SECONDS`, `_MB`) +- Group related constants in namespaces if needed + +### Booleans: Prefix with Question Words + +```typescript +// ✅ Good +interface User { + isActive: boolean; + isDeleted: boolean; + hasVerifiedEmail: boolean; + hasCompletedOnboarding: boolean; + canEditProfile: boolean; + canAccessAdminPanel: boolean; + shouldReceiveNotifications: boolean; +} + +function isValidEmail(email: string): boolean { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +// ❌ Bad +interface User { + active: boolean; // Use isActive + verified: boolean; // Use hasVerifiedEmail + admin: boolean; // Use isAdmin or hasAdminRole +} + +function validateEmail(): boolean {} // Use isValidEmail +``` + +**Prefixes**: + +- `is` - State or condition (`isActive`, `isLoading`) +- `has` - Possession or completion (`hasPermission`, `hasData`) +- `can` - Ability or permission (`canEdit`, `canDelete`) +- `should` - Recommendation or preference (`shouldRetry`, `shouldCache`) + +## Interface vs Implementation Naming + +### No Prefix for Interfaces + +```typescript +// ✅ Good - Clean interface names +export interface UserRepository { + save(user: User): Promise; + findById(id: string): Promise; +} + +export interface PaymentGateway { + charge(amount: number): Promise; +} + +// Implementation uses technology/context prefix +export class PostgresUserRepository implements UserRepository { + async save(user: User): Promise { + // PostgreSQL implementation + } + + async findById(id: string): Promise { + // PostgreSQL implementation + } +} + +export class StripePaymentGateway implements PaymentGateway { + async charge(amount: number): Promise { + // Stripe implementation + } +} + +// ❌ Bad - Hungarian notation for interfaces +export interface IUserRepository {} // Don't use 'I' prefix +export interface IPaymentGateway {} // Don't use 'I' prefix +export class UserRepositoryImpl {} // Don't use 'Impl' suffix +``` + +**Rules**: + +- Interface names describe what it does, not that it's an interface +- Implementation names indicate the technology or context +- Avoid generic suffixes like `Impl`, `Concrete`, `Implementation` + +## DTO and Response Naming + +```typescript +// ✅ Good +export class CreateUserDto { + email: string; + password: string; + name: string; +} + +export class UserResponseDto { + id: string; + email: string; + name: string; + createdAt: Date; +} + +export class UpdateUserDto { + name?: string; + email?: string; +} + +// ❌ Bad +export class UserInput {} // Not descriptive enough +export class UserOutput {} // Not descriptive enough +export class UserDto {} // Ambiguous - for what operation? +``` + +**Patterns**: + +- `Create{Entity}Dto` - For creation operations +- `Update{Entity}Dto` - For update operations +- `{Entity}ResponseDto` - For API responses +- `{Entity}QueryDto` - For query/filter parameters + +## Use Case Naming + +```typescript +// ✅ Good - Verb + noun pattern +export class CreateUserUseCase {} +export class UpdateUserProfileUseCase {} +export class DeleteUserAccountUseCase {} +export class FindUserByEmailUseCase {} +export class AuthenticateUserUseCase {} + +// ❌ Bad +export class UserCreation {} // Use CreateUserUseCase +export class UserService {} // Too generic +export class HandleUser {} // Not descriptive +``` + +**Pattern**: `{Verb}{Entity}{Context}UseCase` + +- Makes intent immediately clear +- Easy to search and organize +- Follows ubiquitous language + +## Principles for Good Naming + +### 1. Intention-Revealing Names + +```typescript +// ✅ Good - Reveals intention +const activeUsersInLastThirtyDays = users.filter( + (u) => u.isActive && u.lastLoginAt > thirtyDaysAgo +); + +// ❌ Bad - Requires mental mapping +const list1 = users.filter((u) => u.a && u.l > d); +``` + +### 2. Avoid Abbreviations + +```typescript +// ✅ Good +const userRepository = new PostgresUserRepository(); +const emailService = new SendGridEmailService(); + +// ❌ Bad +const usrRepo = new PgUsrRepo(); +const emlSvc = new SgEmlSvc(); +``` + +**Exception**: Well-known abbreviations are OK: + +- `id` (identifier) +- `url` (Uniform Resource Locator) +- `api` (Application Programming Interface) +- `dto` (Data Transfer Object) +- `csv`, `json`, `xml` (file formats) + +### 3. Use Domain Language + +```typescript +// ✅ Good - Uses business language +export class SubscriptionRenewalService { + async renewSubscription(subscriptionId: string): Promise { + // Domain-driven naming + } +} + +// ❌ Bad - Uses technical jargon +export class DataProcessor { + async processData(dataId: string): Promise { + // Too generic, doesn't reveal business logic + } +} +``` + +### 4. Make Names Searchable + +```typescript +// ✅ Good - Easy to find in codebase +const DAYS_UNTIL_TRIAL_EXPIRES = 14; +const MAX_LOGIN_ATTEMPTS_BEFORE_LOCKOUT = 5; + +function isTrialExpired(user: User): boolean { + const daysSinceSignup = getDaysSince(user.createdAt); + return daysSinceSignup > DAYS_UNTIL_TRIAL_EXPIRES; +} + +// ❌ Bad - Magic numbers, hard to search +function isTrialExpired(user: User): boolean { + return getDaysSince(user.createdAt) > 14; // What is 14? +} +``` + +### 5. Be Consistent + +```typescript +// ✅ Good - Consistent terminology +async function fetchUserById(id: string): Promise {} +async function fetchOrderById(id: string): Promise {} +async function fetchProductById(id: string): Promise {} + +// ❌ Bad - Inconsistent verbs +async function getUserById(id: string): Promise {} +async function retrieveOrder(id: string): Promise {} +async function loadProduct(id: string): Promise {} +``` + +**Use consistent verbs across the codebase**: + +- `create` / `update` / `delete` for mutations +- `fetch` / `find` / `get` for queries +- `validate` / `check` / `verify` for validation + +## Practical Examples + +### Complete Use Case Example + +```typescript +// ✅ Good - Everything follows conventions + +// create-user.dto.ts +export class CreateUserDto { + email: string; + password: string; + name: string; +} + +// user-response.dto.ts +export class UserResponseDto { + id: string; + email: string; + name: string; + isActive: boolean; + createdAt: Date; +} + +// create-user.use-case.ts +export class CreateUserUseCase { + constructor( + private userRepository: UserRepository, + private passwordHasher: PasswordHasher, + private emailService: EmailService + ) {} + + async execute(dto: CreateUserDto): Promise { + const hashedPassword = await this.passwordHasher.hash(dto.password); + + const user = new User({ + email: dto.email, + password: hashedPassword, + name: dto.name, + }); + + await this.userRepository.save(user); + await this.emailService.sendWelcomeEmail(user.email); + + return this.mapToResponse(user); + } + + private mapToResponse(user: User): UserResponseDto { + return { + id: user.id, + email: user.email, + name: user.name, + isActive: user.isActive, + createdAt: user.createdAt, + }; + } +} +``` + +## Validation Checklist + +Before committing code, verify: + +- [ ] All files use `kebab-case` with appropriate suffixes +- [ ] Directories follow plural/singular conventions +- [ ] Classes and interfaces use `PascalCase` +- [ ] Functions and variables use `camelCase` +- [ ] Constants use `UPPER_SNAKE_CASE` +- [ ] Boolean names start with `is`, `has`, `can`, `should` +- [ ] No abbreviations except well-known ones +- [ ] Names reveal intention without comments +- [ ] Consistent terminology across similar operations +- [ ] Domain language used instead of technical jargon + +## Common Mistakes to Avoid + +1. ❌ Using `any` suffix: `userService`, `userHelper`, `userManager` + + - ✅ Be specific: `UserAuthenticator`, `UserValidator` + +2. ❌ Single-letter variables (except loop counters) + + - ✅ Use descriptive names: `user`, `index`, `accumulator` + +3. ❌ Encoding type in name: `strName`, `arrUsers`, `objConfig` + + - ✅ TypeScript handles types: `name`, `users`, `config` + +4. ❌ Redundant context: `User.userName`, `User.userEmail` + + - ✅ Remove redundancy: `User.name`, `User.email` + +5. ❌ Inconsistent pluralization: `getUserList()`, `fetchUsers()` + - ✅ Pick one pattern: `fetchUsers()`, `fetchOrders()` + +## Remember + +- **Clarity over brevity**: Longer, descriptive names are better than short, cryptic ones +- **Consistency is key**: Follow the same patterns throughout the project +- **Searchability matters**: Someone should be able to find your code by searching logical terms +- **Let the IDE help**: Modern IDEs have autocomplete - don't sacrifice clarity for typing speed diff --git a/skills/project-workflow/SKILL.md b/skills/project-workflow/SKILL.md new file mode 100644 index 0000000..b5af7d7 --- /dev/null +++ b/skills/project-workflow/SKILL.md @@ -0,0 +1,186 @@ +--- +name: project-workflow +description: Development workflow and quality gates for the Bun + TypeScript stack. **ALWAYS use before commits** to ensure quality gates are met. Also use when starting development or when user asks about workflow process. Examples - "before commit", "quality gates", "workflow checklist", "bun commands", "pre-commit checks". +--- + +You are an expert in guiding developers through the project's development workflow and quality gates. You ensure all necessary steps are executed before committing code. + +**For complete project rules and standards, see CLAUDE.md (global instructions)** + +## When to Engage + +You should proactively assist: + +- **Before committing code** (most important) +- When user asks about workflow or process +- When setting up quality gates +- When troubleshooting Bun-specific issues + +## Pre-Commit Checklist + +**MANDATORY - Execute in this order:** + +```bash +# Step 1: Update barrel files (if files were added/moved/deleted) +bun run craft + +# Step 2: Format code +bun run format + +# Step 3: Lint code +bun run lint + +# Step 4: Type check +bun run type-check + +# Step 5: Run tests +bun run test +``` + +**Or run all at once:** + +```bash +bun run quality # Executes all 5 steps +``` + +**Checklist:** + +- [ ] Files added/moved/deleted? Run `bun run craft` +- [ ] All tests passing? (`bun run test` - green) +- [ ] No TypeScript errors? (`bun run type-check` - clean) +- [ ] No linting errors? (`bun run lint` - clean) +- [ ] Code formatted? (`bun run format` - applied) +- [ ] Committed to feature branch? (not main/dev) +- [ ] Commit message follows conventions? + +**For complete TypeScript type safety rules (type guards), see `typescript-type-safety` skill** + +## Bun-Specific Commands + +### Testing Commands + +**CRITICAL - NEVER use:** + +```bash +bun test # ❌ WRONG - May not work correctly +``` + +**ALWAYS use:** + +```bash +bun run test # ✅ CORRECT - Uses package.json script +``` + +### Barrel Files + +**ALWAYS run after creating/moving/deleting files:** + +```bash +bun run craft +``` + +This updates barrel files (index.ts exports) for clean imports. + +**When to run:** + +- After creating new files +- After moving/renaming files +- After deleting files +- Before committing changes + +## Bun Runtime APIs + +**Prefer Bun APIs over Node.js:** + +```typescript +// ✅ Password hashing +const hashedPassword = await Bun.password.hash(password, { + algorithm: "bcrypt", + cost: 10, +}); + +// ✅ File operations +const file = Bun.file("./config.json"); +const config = await file.json(); + +// ✅ UUID v7 +const id = Bun.randomUUIDv7(); + +// ✅ SQLite +import { Database } from "bun:sqlite"; +const db = new Database("mydb.sqlite"); + +// ✅ HTTP server +import { serve } from "bun"; +serve({ + port: 3000, + fetch(req) { + return new Response("Hello from Bun!"); + }, +}); +``` + +## Quality Gates Execution Order + +**Why this order matters:** + +1. **craft** - Ensures imports are correct before other checks +2. **format** - Auto-fixes formatting issues +3. **lint** - Catches code quality issues +4. **type-check** - Validates TypeScript correctness +5. **test** - Ensures functionality works + +**Each step depends on the previous one passing.** + +## Common Workflow Mistakes + +**Mistakes to avoid:** + +1. ❌ Using `bun test` instead of `bun run test` +2. ❌ Forgetting `bun run craft` after file operations +3. ❌ Committing with TypeScript errors +4. ❌ Skipping quality gates +5. ❌ Running quality gates out of order +6. ❌ Committing directly to main/dev branches +7. ❌ Using Node.js APIs instead of Bun APIs +8. ❌ Relative imports instead of barrel files + +## Quick Reference + +**Before every commit:** + +```bash +bun run quality # Run all quality gates +git status # Verify changes +git add . # Stage changes +git commit -m "feat(scope): description" # Commit with convention +``` + +**Starting new feature:** + +```bash +git checkout dev +git pull origin dev +git checkout -b feature/feature-name +# ... make changes ... +bun run quality +git commit -m "feat: add feature" +``` + +**File operations workflow:** + +```bash +# Create new files +# ... +bun run craft # Update barrel files +bun run quality # Run quality gates +git commit +``` + +## Remember + +- **Quality gates are mandatory** - Not optional +- **Bun commands are specific** - Use `bun run test`, not `bun test` +- **Order matters** - Follow the quality gates sequence +- **Barrel files are critical** - Run `bun run craft` after file changes +- **Check CLAUDE.md** - For complete project rules and standards diff --git a/skills/typescript-type-safety/SKILL.md b/skills/typescript-type-safety/SKILL.md new file mode 100644 index 0000000..2c9f7a8 --- /dev/null +++ b/skills/typescript-type-safety/SKILL.md @@ -0,0 +1,507 @@ +--- +name: typescript-type-safety +description: TypeScript type safety including type guards and advanced type system features. **ALWAYS use when writing ANY TypeScript code (frontend AND backend)** to ensure strict type safety, avoid `any` types, and leverage the type system. Examples - "create function", "implement class", "define interface", "type guard", "discriminated union", "type narrowing", "conditional types", "handle unknown types". +--- + +You are an expert in TypeScript's type system and type safety. You guide developers to write type-safe code that leverages TypeScript's powerful type system to catch errors at compile time. + +**For development workflow and quality gates (pre-commit checklist, bun commands), see `project-workflow` skill** + +## When to Engage + +You should proactively assist when: + +- Working with `unknown` types in any context +- Implementing type guards for context-specific types +- Using discriminated unions within bounded contexts +- Implementing advanced TypeScript patterns without over-abstraction +- User asks about type safety or TypeScript features + +## Modular Monolith Type Safety + +### Context-Specific Types + +```typescript +// ✅ GOOD: Each context owns its types +// contexts/auth/domain/types/user.types.ts +export interface AuthUser { + id: string; + email: string; + isActive: boolean; +} + +// contexts/tax/domain/types/calculation.types.ts +export interface TaxCalculation { + ncmCode: string; + rate: number; + amount: number; +} + +// ❌ BAD: Shared generic types that couple contexts +// shared/types/base.types.ts +export interface BaseEntity { + // NO! Creates coupling + id: string; + data: T; +} +``` + +## Core Type Safety Rules + +### 1. NEVER Use `any` + +```typescript +// ❌ FORBIDDEN - Disables type checking +function process(data: any) { + return data.value; // No type safety at all +} + +// ✅ CORRECT - Use unknown with type guards +function process(data: unknown): string { + if (isProcessData(data)) { + return data.value; // Type-safe access + } + throw new TypeError("Invalid data structure"); +} + +interface ProcessData { + value: string; +} + +function isProcessData(data: unknown): data is ProcessData { + return ( + typeof data === "object" && + data !== null && + "value" in data && + typeof (data as ProcessData).value === "string" + ); +} +``` + +### 2. Use Proper Type Guards + +```typescript +// ✅ Type predicate (narrows type) +function isString(value: unknown): value is string { + return typeof value === "string"; +} + +function isNumber(value: unknown): value is number { + return typeof value === "number"; +} + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +// ✅ Complex type guard +interface User { + id: string; + email: string; + name: string; +} + +function isUser(value: unknown): value is User { + if (!isObject(value)) return false; + + return ( + "id" in value && + typeof value.id === "string" && + "email" in value && + typeof value.email === "string" && + "name" in value && + typeof value.name === "string" + ); +} + +// Usage +function processUser(data: unknown): User { + if (!isUser(data)) { + throw new TypeError("Invalid user data"); + } + // TypeScript knows data is User here + return data; +} +``` + +## Discriminated Unions + +```typescript +// ✅ Discriminated unions for polymorphic data +type PaymentMethod = + | { + type: "credit_card"; + cardNumber: string; + expiryDate: string; + cvv: string; + } + | { + type: "paypal"; + email: string; + } + | { + type: "bank_transfer"; + accountNumber: string; + routingNumber: string; + }; + +function processPayment(method: PaymentMethod, amount: number): void { + switch (method.type) { + case "credit_card": + // TypeScript knows method has cardNumber, expiryDate, cvv + console.log(`Charging ${amount} to card ${method.cardNumber}`); + break; + + case "paypal": + // TypeScript knows method has email + console.log(`Charging ${amount} to PayPal ${method.email}`); + break; + + case "bank_transfer": + // TypeScript knows method has accountNumber, routingNumber + console.log( + `Charging ${amount} via bank transfer ${method.accountNumber}` + ); + break; + + default: + // Exhaustiveness check + const _exhaustive: never = method; + throw new Error(`Unhandled payment method: ${_exhaustive}`); + } +} +``` + +## Conditional Types + +```typescript +// ✅ Conditional types for flexible APIs +type ResponseData = T extends { id: string } + ? { success: true; data: T } + : never; + +type User = { id: string; name: string }; +type UserResponse = ResponseData; // { success: true; data: User } + +// ✅ Extract promise type +type Awaited = T extends Promise ? U : T; + +type UserPromise = Promise; +type UserType = Awaited; // User + +// ✅ Extract function return type +type ReturnType = T extends (...args: unknown[]) => infer R ? R : never; + +function getUser(): User { + return { id: "1", name: "John" }; +} + +type UserFromFunction = ReturnType; // User +``` + +## Mapped Types + +```typescript +// ✅ Make all properties optional +type Partial = { + [P in keyof T]?: T[P]; +}; + +type User = { + id: string; + email: string; + name: string; +}; + +type PartialUser = Partial; +// { id?: string; email?: string; name?: string } + +// ✅ Make all properties readonly +type Readonly = { + readonly [P in keyof T]: T[P]; +}; + +type ReadonlyUser = Readonly; + +// ✅ Pick specific properties +type Pick = { + [P in K]: T[P]; +}; + +type UserPreview = Pick; +// { id: string; name: string } + +// ✅ Omit specific properties +type Omit = Pick>; + +type UserWithoutId = Omit; +// { email: string; name: string } +``` + +## Template Literal Types + +```typescript +// ✅ Type-safe string patterns +type EventName = "user" | "order" | "payment"; +type EventAction = "created" | "updated" | "deleted"; + +type Event = `${EventName}:${EventAction}`; +// 'user:created' | 'user:updated' | 'user:deleted' | +// 'order:created' | 'order:updated' | 'order:deleted' | +// 'payment:created' | 'payment:updated' | 'payment:deleted' + +function emitEvent(event: Event): void { + console.log(`Emitting ${event}`); +} + +emitEvent("user:created"); // ✅ OK +emitEvent("user:invalid"); // ❌ Type error +``` + +## Function Overloads + +```typescript +// ✅ Function overloads for different input/output types +function getValue(key: "count"): number; +function getValue(key: "name"): string; +function getValue(key: "isActive"): boolean; +function getValue(key: string): unknown { + // Implementation + const values: Record = { + count: 42, + name: "John", + isActive: true, + }; + return values[key]; +} + +const count = getValue("count"); // Type is number +const name = getValue("name"); // Type is string +const isActive = getValue("isActive"); // Type is boolean +``` + +## Const Assertions + +```typescript +// ✅ Const assertions for literal types +const config = { + apiUrl: "https://api.example.com", + timeout: 5000, + retries: 3, +} as const; + +// config.apiUrl type is 'https://api.example.com' (literal) +// config.timeout type is 5000 (literal) +// config is readonly + +// ✅ Array as const +const colors = ["red", "green", "blue"] as const; +// Type is readonly ['red', 'green', 'blue'] + +type Color = (typeof colors)[number]; +// Type is 'red' | 'green' | 'blue' +``` + +## Utility Types + +### NonNullable + +```typescript +type NonNullable = T extends null | undefined ? never : T; + +type MaybeString = string | null | undefined; +type DefiniteString = NonNullable; // string +``` + +### Extract and Exclude + +```typescript +type Extract = T extends U ? T : never; +type Exclude = T extends U ? never : T; + +type Status = "pending" | "approved" | "rejected" | "cancelled"; + +type PositiveStatus = Extract; +// 'approved' | 'pending' + +type NegativeStatus = Exclude; +// 'rejected' | 'cancelled' +``` + +### Record + +```typescript +type Record = { + [P in K]: T; +}; + +type UserRoles = "admin" | "user" | "guest"; +type Permissions = Record; +// { +// admin: string[]; +// user: string[]; +// guest: string[]; +// } + +const permissions: Permissions = { + admin: ["read", "write", "delete"], + user: ["read", "write"], + guest: ["read"], +}; +``` + +## Type Narrowing + +### typeof Guards + +```typescript +function process(value: string | number): string { + if (typeof value === "string") { + return value.toUpperCase(); // value is string here + } + return value.toFixed(2); // value is number here +} +``` + +### instanceof Guards + +```typescript +class User { + constructor(public name: string) {} +} + +class Admin extends User { + constructor(name: string, public level: number) { + super(name); + } +} + +function greet(user: User | Admin): string { + if (user instanceof Admin) { + return `Hello Admin ${user.name}, level ${user.level}`; + } + return `Hello ${user.name}`; +} +``` + +### in Operator + +```typescript +type Dog = { bark: () => void }; +type Cat = { meow: () => void }; + +function makeSound(animal: Dog | Cat): void { + if ("bark" in animal) { + animal.bark(); // animal is Dog here + } else { + animal.meow(); // animal is Cat here + } +} +``` + +## TypeScript Configuration + +```json +{ + "compilerOptions": { + // Strict type checking + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Module resolution + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + + // Interop + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + + // Other + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + // Bun-specific + "types": ["bun-types"], + "target": "ES2022", + "lib": ["ES2022"] + } +} +``` + +## Best Practices + +### Do: + +- ✅ Use `unknown` instead of `any` +- ✅ Implement type guards for runtime checks +- ✅ Leverage discriminated unions for polymorphism +- ✅ Enable strict mode in tsconfig.json +- ✅ Use const assertions for literal types +- ✅ Implement exhaustiveness checks in switch statements + +### Don't: + +- ❌ Use `any` type +- ❌ Use type assertions (as) without validation +- ❌ Disable strict mode +- ❌ Use `@ts-ignore` or `@ts-expect-error` without good reason +- ❌ Mix types and interfaces unnecessarily +- ❌ Create overly complex type gymnastics + +## Common Type Errors and Solutions + +### Error: Object is possibly 'null' or 'undefined' + +```typescript +// ❌ Error +function getName(user: User | null): string { + return user.name; // Error: Object is possibly 'null' +} + +// ✅ Solution: Check for null +function getName(user: User | null): string { + if (!user) { + throw new Error("User is null"); + } + return user.name; // OK +} + +// ✅ Or use optional chaining +function getName(user: User | null): string | undefined { + return user?.name; +} +``` + +### Error: Type 'X' is not assignable to type 'Y' + +```typescript +// ❌ Error +interface User { + id: string; + name: string; +} + +const user = { + id: "1", + name: "John", + extra: "field", +}; + +const typedUser: User = user; // OK (structural typing) + +// ✅ Use type annotation or assertion when needed +const exactUser: User = { + id: "1", + name: "John", + // extra: 'field', // Error if uncommented +}; +``` + +## Remember + +- **Type safety catches bugs at compile time** - Invest in good types +- **unknown > any** - Always use unknown for truly unknown types +- **Type guards are your friends** - Use them liberally +- **Strict mode is mandatory** - Never disable it