@ggailabs/cli-context 0.5.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @ggailabs/cli-context might be problematic. Click here for more details.
- package/dist/.context/docs/GENESIS_SYSTEM_PROMPT.md +62 -0
- package/dist/.context/eng/agents/architect.md +15 -0
- package/dist/.context/eng/agents/backend_typescript.md +1000 -0
- package/dist/.context/eng/agents/bug_hunter.md +49 -0
- package/dist/.context/eng/agents/code_reviewer.md +313 -0
- package/dist/.context/eng/agents/devops_specialist.md +718 -0
- package/dist/.context/eng/agents/frontend_specialist.md +1027 -0
- package/dist/.context/eng/agents/qa_specialist.md +1234 -0
- package/dist/.context/eng/agents/security_reviewer.md +382 -0
- package/dist/.context/eng/agents/ui_specialist.md +16 -0
- package/dist/.context/eng/docs/AGENT_HANDOFF_PROMPT.md +44 -0
- package/dist/.context/eng/docs/GENESIS_DESIGN_SYSTEM.md +15 -0
- package/dist/.context/eng/docs/GG_METHODOLOGY.md +48 -0
- package/dist/.context/eng/docs/OPERATIONS_LOG.md +30 -0
- package/dist/.context/eng/docs/PROJECT_MAP.md +35 -0
- package/dist/.context/eng/docs/standards/devops.md +0 -0
- package/dist/.context/eng/docs/standards/frontend.md +0 -0
- package/dist/.context/eng/docs/standards/typescript.md +12 -0
- package/dist/.context/eng/skills/agent_logging.md +18 -0
- package/dist/.context/eng/skills/brainstorming.md +227 -0
- package/dist/.context/eng/skills/exploring_codebase.md +1217 -0
- package/dist/.context/eng/skills/patterns/compliance_check.md +246 -0
- package/dist/.context/eng/skills/patterns/coverage_table.md +401 -0
- package/dist/.context/eng/skills/patterns/exit_criteria.md +31 -0
- package/dist/.context/eng/skills/patterns/failure_recovery.md +74 -0
- package/dist/.context/eng/skills/patterns/quality_gate.md +295 -0
- package/dist/.context/eng/skills/patterns/standards_workflow.md +391 -0
- package/dist/.context/eng/skills/patterns/state_tracking.md +30 -0
- package/dist/.context/eng/skills/patterns/task_tracking.md +38 -0
- package/dist/.context/eng/skills/previce.md +45 -0
- package/dist/.context/eng/skills/tdd.md +421 -0
- package/dist/.context/eng/skills/writing_plans.md +105 -0
- package/dist/.context/plans/.gitkeep +0 -0
- package/dist/.context/pt-br/agents/architect.md +69 -0
- package/dist/.context/pt-br/agents/backend_typescript.md +1000 -0
- package/dist/.context/pt-br/agents/bug_hunter.md +49 -0
- package/dist/.context/pt-br/agents/code_reviewer.md +313 -0
- package/dist/.context/pt-br/agents/devops_specialist.md +718 -0
- package/dist/.context/pt-br/agents/frontend_specialist.md +1027 -0
- package/dist/.context/pt-br/agents/qa_specialist.md +1234 -0
- package/dist/.context/pt-br/agents/security_reviewer.md +382 -0
- package/dist/.context/pt-br/agents/ui_specialist.md +55 -0
- package/dist/.context/pt-br/docs/AGENT_HANDOFF_PROMPT.md +44 -0
- package/dist/.context/pt-br/docs/GENESIS_DESIGN_SYSTEM.md +88 -0
- package/dist/.context/pt-br/docs/GG_METHODOLOGY.md +48 -0
- package/dist/.context/pt-br/docs/OPERATIONS_LOG.md +30 -0
- package/dist/.context/pt-br/docs/PROJECT_MAP.md +37 -0
- package/dist/.context/pt-br/docs/standards/devops.md +707 -0
- package/dist/.context/pt-br/docs/standards/frontend.md +748 -0
- package/dist/.context/pt-br/docs/standards/typescript.md +1150 -0
- package/dist/.context/pt-br/skills/agent_logging.md +36 -0
- package/dist/.context/pt-br/skills/brainstorming.md +227 -0
- package/dist/.context/pt-br/skills/exploring_codebase.md +1217 -0
- package/dist/.context/pt-br/skills/patterns/compliance_check.md +246 -0
- package/dist/.context/pt-br/skills/patterns/coverage_table.md +401 -0
- package/dist/.context/pt-br/skills/patterns/exit_criteria.md +31 -0
- package/dist/.context/pt-br/skills/patterns/failure_recovery.md +74 -0
- package/dist/.context/pt-br/skills/patterns/quality_gate.md +295 -0
- package/dist/.context/pt-br/skills/patterns/standards_workflow.md +391 -0
- package/dist/.context/pt-br/skills/patterns/state_tracking.md +30 -0
- package/dist/.context/pt-br/skills/patterns/task_tracking.md +38 -0
- package/dist/.context/pt-br/skills/previce.md +45 -0
- package/dist/.context/pt-br/skills/tdd.md +421 -0
- package/dist/.context/pt-br/skills/writing_plans.md +105 -0
- package/dist/.context/workflow/.gitkeep +0 -0
- package/dist/commands/init.js +140 -0
- package/dist/commands/monitor.js +34 -0
- package/dist/index.js +20 -568
- package/dist/services/monitor-service.js +340 -0
- package/dist/services/scaffolder.js +164 -0
- package/package.json +16 -58
- package/LICENSE +0 -21
- package/README.md +0 -197
- package/dist/generators/agents/agentConfig.d.ts +0 -4
- package/dist/generators/agents/agentConfig.d.ts.map +0 -1
- package/dist/generators/agents/agentConfig.js +0 -180
- package/dist/generators/agents/agentConfig.js.map +0 -1
- package/dist/generators/agents/agentGenerator.d.ts +0 -9
- package/dist/generators/agents/agentGenerator.d.ts.map +0 -1
- package/dist/generators/agents/agentGenerator.js +0 -97
- package/dist/generators/agents/agentGenerator.js.map +0 -1
- package/dist/generators/agents/agentTypes.d.ts +0 -4
- package/dist/generators/agents/agentTypes.d.ts.map +0 -1
- package/dist/generators/agents/agentTypes.js +0 -25
- package/dist/generators/agents/agentTypes.js.map +0 -1
- package/dist/generators/agents/index.d.ts +0 -4
- package/dist/generators/agents/index.d.ts.map +0 -1
- package/dist/generators/agents/index.js +0 -12
- package/dist/generators/agents/index.js.map +0 -1
- package/dist/generators/agents/templates/index.d.ts +0 -4
- package/dist/generators/agents/templates/index.d.ts.map +0 -1
- package/dist/generators/agents/templates/index.js +0 -8
- package/dist/generators/agents/templates/index.js.map +0 -1
- package/dist/generators/agents/templates/indexTemplate.d.ts +0 -3
- package/dist/generators/agents/templates/indexTemplate.d.ts.map +0 -1
- package/dist/generators/agents/templates/indexTemplate.js +0 -36
- package/dist/generators/agents/templates/indexTemplate.js.map +0 -1
- package/dist/generators/agents/templates/playbookTemplate.d.ts +0 -4
- package/dist/generators/agents/templates/playbookTemplate.d.ts.map +0 -1
- package/dist/generators/agents/templates/playbookTemplate.js +0 -99
- package/dist/generators/agents/templates/playbookTemplate.js.map +0 -1
- package/dist/generators/agents/templates/types.d.ts +0 -14
- package/dist/generators/agents/templates/types.d.ts.map +0 -1
- package/dist/generators/agents/templates/types.js +0 -3
- package/dist/generators/agents/templates/types.js.map +0 -1
- package/dist/generators/documentation/documentationGenerator.d.ts +0 -15
- package/dist/generators/documentation/documentationGenerator.d.ts.map +0 -1
- package/dist/generators/documentation/documentationGenerator.js +0 -188
- package/dist/generators/documentation/documentationGenerator.js.map +0 -1
- package/dist/generators/documentation/guideRegistry.d.ts +0 -6
- package/dist/generators/documentation/guideRegistry.d.ts.map +0 -1
- package/dist/generators/documentation/guideRegistry.js +0 -82
- package/dist/generators/documentation/guideRegistry.js.map +0 -1
- package/dist/generators/documentation/index.d.ts +0 -2
- package/dist/generators/documentation/index.d.ts.map +0 -1
- package/dist/generators/documentation/index.js +0 -6
- package/dist/generators/documentation/index.js.map +0 -1
- package/dist/generators/documentation/templates/apiReferenceTemplate.d.ts +0 -2
- package/dist/generators/documentation/templates/apiReferenceTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/apiReferenceTemplate.js +0 -490
- package/dist/generators/documentation/templates/apiReferenceTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/architectureTemplate.d.ts +0 -3
- package/dist/generators/documentation/templates/architectureTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/architectureTemplate.js +0 -66
- package/dist/generators/documentation/templates/architectureTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/common.d.ts +0 -7
- package/dist/generators/documentation/templates/common.d.ts.map +0 -1
- package/dist/generators/documentation/templates/common.js +0 -58
- package/dist/generators/documentation/templates/common.js.map +0 -1
- package/dist/generators/documentation/templates/dataFlowTemplate.d.ts +0 -3
- package/dist/generators/documentation/templates/dataFlowTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/dataFlowTemplate.js +0 -40
- package/dist/generators/documentation/templates/dataFlowTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/developmentWorkflowTemplate.d.ts +0 -2
- package/dist/generators/documentation/templates/developmentWorkflowTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/developmentWorkflowTemplate.js +0 -44
- package/dist/generators/documentation/templates/developmentWorkflowTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/glossaryTemplate.d.ts +0 -3
- package/dist/generators/documentation/templates/glossaryTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/glossaryTemplate.js +0 -41
- package/dist/generators/documentation/templates/glossaryTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/index.d.ts +0 -15
- package/dist/generators/documentation/templates/index.d.ts.map +0 -1
- package/dist/generators/documentation/templates/index.js +0 -30
- package/dist/generators/documentation/templates/index.js.map +0 -1
- package/dist/generators/documentation/templates/indexTemplate.d.ts +0 -3
- package/dist/generators/documentation/templates/indexTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/indexTemplate.js +0 -42
- package/dist/generators/documentation/templates/indexTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/migrationTemplate.d.ts +0 -2
- package/dist/generators/documentation/templates/migrationTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/migrationTemplate.js +0 -422
- package/dist/generators/documentation/templates/migrationTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/onboardingTemplate.d.ts +0 -2
- package/dist/generators/documentation/templates/onboardingTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/onboardingTemplate.js +0 -431
- package/dist/generators/documentation/templates/onboardingTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/projectOverviewTemplate.d.ts +0 -3
- package/dist/generators/documentation/templates/projectOverviewTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/projectOverviewTemplate.js +0 -65
- package/dist/generators/documentation/templates/projectOverviewTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/securityTemplate.d.ts +0 -2
- package/dist/generators/documentation/templates/securityTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/securityTemplate.js +0 -39
- package/dist/generators/documentation/templates/securityTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/testingTemplate.d.ts +0 -2
- package/dist/generators/documentation/templates/testingTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/testingTemplate.js +0 -45
- package/dist/generators/documentation/templates/testingTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/toolingTemplate.d.ts +0 -2
- package/dist/generators/documentation/templates/toolingTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/toolingTemplate.js +0 -42
- package/dist/generators/documentation/templates/toolingTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/troubleshootingTemplate.d.ts +0 -2
- package/dist/generators/documentation/templates/troubleshootingTemplate.d.ts.map +0 -1
- package/dist/generators/documentation/templates/troubleshootingTemplate.js +0 -292
- package/dist/generators/documentation/templates/troubleshootingTemplate.js.map +0 -1
- package/dist/generators/documentation/templates/types.d.ts +0 -23
- package/dist/generators/documentation/templates/types.d.ts.map +0 -1
- package/dist/generators/documentation/templates/types.js +0 -3
- package/dist/generators/documentation/templates/types.js.map +0 -1
- package/dist/generators/plans/index.d.ts +0 -2
- package/dist/generators/plans/index.d.ts.map +0 -1
- package/dist/generators/plans/index.js +0 -6
- package/dist/generators/plans/index.js.map +0 -1
- package/dist/generators/plans/planGenerator.d.ts +0 -22
- package/dist/generators/plans/planGenerator.d.ts.map +0 -1
- package/dist/generators/plans/planGenerator.js +0 -109
- package/dist/generators/plans/planGenerator.js.map +0 -1
- package/dist/generators/plans/templates/indexTemplate.d.ts +0 -3
- package/dist/generators/plans/templates/indexTemplate.d.ts.map +0 -1
- package/dist/generators/plans/templates/indexTemplate.js +0 -37
- package/dist/generators/plans/templates/indexTemplate.js.map +0 -1
- package/dist/generators/plans/templates/planTemplate.d.ts +0 -3
- package/dist/generators/plans/templates/planTemplate.d.ts.map +0 -1
- package/dist/generators/plans/templates/planTemplate.js +0 -166
- package/dist/generators/plans/templates/planTemplate.js.map +0 -1
- package/dist/generators/plans/templates/types.d.ts +0 -19
- package/dist/generators/plans/templates/types.d.ts.map +0 -1
- package/dist/generators/plans/templates/types.js +0 -3
- package/dist/generators/plans/templates/types.js.map +0 -1
- package/dist/generators/shared/contextGenerator.d.ts +0 -7
- package/dist/generators/shared/contextGenerator.d.ts.map +0 -1
- package/dist/generators/shared/contextGenerator.js +0 -13
- package/dist/generators/shared/contextGenerator.js.map +0 -1
- package/dist/generators/shared/directoryTemplateHelpers.d.ts +0 -2
- package/dist/generators/shared/directoryTemplateHelpers.d.ts.map +0 -1
- package/dist/generators/shared/directoryTemplateHelpers.js +0 -12
- package/dist/generators/shared/directoryTemplateHelpers.js.map +0 -1
- package/dist/generators/shared/generatorUtils.d.ts +0 -16
- package/dist/generators/shared/generatorUtils.d.ts.map +0 -1
- package/dist/generators/shared/generatorUtils.js +0 -119
- package/dist/generators/shared/generatorUtils.js.map +0 -1
- package/dist/generators/shared/index.d.ts +0 -4
- package/dist/generators/shared/index.d.ts.map +0 -1
- package/dist/generators/shared/index.js +0 -10
- package/dist/generators/shared/index.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/prompts/defaults.d.ts +0 -3
- package/dist/prompts/defaults.d.ts.map +0 -1
- package/dist/prompts/defaults.js +0 -95
- package/dist/prompts/defaults.js.map +0 -1
- package/dist/services/baseLLMClient.d.ts +0 -12
- package/dist/services/baseLLMClient.d.ts.map +0 -1
- package/dist/services/baseLLMClient.js +0 -80
- package/dist/services/baseLLMClient.js.map +0 -1
- package/dist/services/fill/fillService.d.ts +0 -46
- package/dist/services/fill/fillService.d.ts.map +0 -1
- package/dist/services/fill/fillService.js +0 -254
- package/dist/services/fill/fillService.js.map +0 -1
- package/dist/services/init/initService.d.ts +0 -37
- package/dist/services/init/initService.d.ts.map +0 -1
- package/dist/services/init/initService.js +0 -167
- package/dist/services/init/initService.js.map +0 -1
- package/dist/services/llmClientFactory.d.ts +0 -8
- package/dist/services/llmClientFactory.d.ts.map +0 -1
- package/dist/services/llmClientFactory.js +0 -23
- package/dist/services/llmClientFactory.js.map +0 -1
- package/dist/services/openRouterClient.d.ts +0 -9
- package/dist/services/openRouterClient.d.ts.map +0 -1
- package/dist/services/openRouterClient.js +0 -49
- package/dist/services/openRouterClient.js.map +0 -1
- package/dist/services/plan/planService.d.ts +0 -57
- package/dist/services/plan/planService.d.ts.map +0 -1
- package/dist/services/plan/planService.js +0 -334
- package/dist/services/plan/planService.js.map +0 -1
- package/dist/services/shared/llmConfig.d.ts +0 -22
- package/dist/services/shared/llmConfig.d.ts.map +0 -1
- package/dist/services/shared/llmConfig.js +0 -38
- package/dist/services/shared/llmConfig.js.map +0 -1
- package/dist/types.d.ts +0 -65
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/utils/cliUI.d.ts +0 -27
- package/dist/utils/cliUI.d.ts.map +0 -1
- package/dist/utils/cliUI.js +0 -252
- package/dist/utils/cliUI.js.map +0 -1
- package/dist/utils/fileMapper.d.ts +0 -11
- package/dist/utils/fileMapper.d.ts.map +0 -1
- package/dist/utils/fileMapper.js +0 -146
- package/dist/utils/fileMapper.js.map +0 -1
- package/dist/utils/gitService.d.ts +0 -50
- package/dist/utils/gitService.d.ts.map +0 -1
- package/dist/utils/gitService.js +0 -470
- package/dist/utils/gitService.js.map +0 -1
- package/dist/utils/i18n.d.ts +0 -171
- package/dist/utils/i18n.d.ts.map +0 -1
- package/dist/utils/i18n.js +0 -381
- package/dist/utils/i18n.js.map +0 -1
- package/dist/utils/promptLoader.d.ts +0 -12
- package/dist/utils/promptLoader.d.ts.map +0 -1
- package/dist/utils/promptLoader.js +0 -81
- package/dist/utils/promptLoader.js.map +0 -1
- package/dist/utils/versionChecker.d.ts +0 -15
- package/dist/utils/versionChecker.d.ts.map +0 -1
- package/dist/utils/versionChecker.js +0 -49
- package/dist/utils/versionChecker.js.map +0 -1
- package/prompts/update_plan_prompt.md +0 -41
- package/prompts/update_scaffold_prompt.md +0 -47
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
# TypeScript Standards
|
|
2
|
+
|
|
3
|
+
> **⚠️ MAINTENANCE:** This file is indexed in `dev-team/skills/shared-patterns/standards-coverage-table.md`.
|
|
4
|
+
> When adding/removing `## ` sections, follow FOUR-FILE UPDATE RULE in CLAUDE.md: (1) edit standards file, (2) update TOC, (3) update standards-coverage-table.md, (4) update agent file.
|
|
5
|
+
|
|
6
|
+
This file defines the specific standards for TypeScript (backend) development.
|
|
7
|
+
|
|
8
|
+
> **Reference**: Always consult `docs/PROJECT_RULES.md` for common project standards.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
| # | Section | Description |
|
|
15
|
+
|---|---------|-------------|
|
|
16
|
+
| 1 | [Version](#version) | TypeScript and Node.js versions |
|
|
17
|
+
| 2 | [Strict Configuration](#strict-configuration-mandatory) | tsconfig.json requirements |
|
|
18
|
+
| 3 | [Frameworks & Libraries](#frameworks--libraries) | Required packages |
|
|
19
|
+
| 4 | [Type Safety](#type-safety) | Never use any, branded types |
|
|
20
|
+
| 5 | [Zod Validation Patterns](#zod-validation-patterns) | Schema validation |
|
|
21
|
+
| 6 | [Dependency Injection](#dependency-injection) | TSyringe patterns |
|
|
22
|
+
| 7 | [AsyncLocalStorage for Context](#asynclocalstorage-for-context) | Request context propagation |
|
|
23
|
+
| 8 | [Testing](#testing) | Type-safe mocks, fixtures |
|
|
24
|
+
| 9 | [Error Handling](#error-handling) | Custom error classes |
|
|
25
|
+
| 10 | [Function Design](#function-design-mandatory) | Single responsibility principle |
|
|
26
|
+
| 11 | [Naming Conventions](#naming-conventions) | Files, interfaces, types |
|
|
27
|
+
| 12 | [Directory Structure](#directory-structure) | Project layout (Lerian pattern) |
|
|
28
|
+
| 13 | [RabbitMQ Worker Pattern](#rabbitmq-worker-pattern) | Async message processing |
|
|
29
|
+
| 14 | [Always-Valid Domain Model](#always-valid-domain-model-mandatory) | Constructor validation, invariant protection |
|
|
30
|
+
|
|
31
|
+
**Meta-sections (not checked by agents):**
|
|
32
|
+
- [Checklist](#checklist) - Self-verification before submitting code
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Version
|
|
37
|
+
|
|
38
|
+
- TypeScript 5.0+
|
|
39
|
+
- Node.js 20+ / Deno 1.40+ / Bun 1.0+
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Strict Configuration (MANDATORY)
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"compilerOptions": {
|
|
48
|
+
"strict": true,
|
|
49
|
+
"noUncheckedIndexedAccess": true,
|
|
50
|
+
"noImplicitOverride": true,
|
|
51
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
52
|
+
"exactOptionalPropertyTypes": true,
|
|
53
|
+
"noFallthroughCasesInSwitch": true,
|
|
54
|
+
"noImplicitReturns": true,
|
|
55
|
+
"forceConsistentCasingInFileNames": true,
|
|
56
|
+
"skipLibCheck": false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Frameworks & Libraries
|
|
64
|
+
|
|
65
|
+
### Backend Frameworks
|
|
66
|
+
|
|
67
|
+
| Framework | Use Case |
|
|
68
|
+
|-----------|----------|
|
|
69
|
+
| Express | Traditional, widely adopted |
|
|
70
|
+
| Fastify | High performance |
|
|
71
|
+
| NestJS | Enterprise, Angular-style DI |
|
|
72
|
+
| Hono | Ultrafast, edge-ready |
|
|
73
|
+
| tRPC | End-to-end type safety |
|
|
74
|
+
|
|
75
|
+
### ORMs & Query Builders
|
|
76
|
+
|
|
77
|
+
| Library | Use Case |
|
|
78
|
+
|---------|----------|
|
|
79
|
+
| Prisma | Type-safe ORM, migrations |
|
|
80
|
+
| Drizzle | Lightweight, SQL-like |
|
|
81
|
+
| TypeORM | Decorator-based ORM |
|
|
82
|
+
| Kysely | Type-safe query builder |
|
|
83
|
+
|
|
84
|
+
### Validation
|
|
85
|
+
|
|
86
|
+
| Library | Use Case |
|
|
87
|
+
|---------|----------|
|
|
88
|
+
| Zod | Schema validation + types |
|
|
89
|
+
| Yup | Object schema validation |
|
|
90
|
+
| joi | Classic validation |
|
|
91
|
+
| class-validator | Decorator-based |
|
|
92
|
+
|
|
93
|
+
### Testing
|
|
94
|
+
|
|
95
|
+
| Library | Use Case |
|
|
96
|
+
|---------|----------|
|
|
97
|
+
| Vitest | Fast, Vite-native |
|
|
98
|
+
| Jest | Full-featured |
|
|
99
|
+
| Supertest | HTTP testing |
|
|
100
|
+
| testcontainers | Integration tests |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Type Safety
|
|
105
|
+
|
|
106
|
+
### never use `any`
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// FORBIDDEN
|
|
110
|
+
const data: any = fetchData();
|
|
111
|
+
function process(x: any) { ... }
|
|
112
|
+
|
|
113
|
+
// CORRECT - use unknown with type narrowing
|
|
114
|
+
const data: unknown = fetchData();
|
|
115
|
+
if (isUser(data)) {
|
|
116
|
+
console.log(data.name); // Now TypeScript knows it's User
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Type guard
|
|
120
|
+
function isUser(value: unknown): value is User {
|
|
121
|
+
return (
|
|
122
|
+
typeof value === 'object' &&
|
|
123
|
+
value !== null &&
|
|
124
|
+
'id' in value &&
|
|
125
|
+
'name' in value
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Branded Types for IDs
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// Define branded type to prevent ID mixing
|
|
134
|
+
type Brand<T, B> = T & { __brand: B };
|
|
135
|
+
|
|
136
|
+
type UserId = Brand<string, 'UserId'>;
|
|
137
|
+
type TenantId = Brand<string, 'TenantId'>;
|
|
138
|
+
type OrderId = Brand<string, 'OrderId'>;
|
|
139
|
+
|
|
140
|
+
// Factory functions with validation
|
|
141
|
+
function createUserId(value: string): UserId {
|
|
142
|
+
if (!value.startsWith('usr_')) {
|
|
143
|
+
throw new Error('Invalid user ID format');
|
|
144
|
+
}
|
|
145
|
+
return value as UserId;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Now TypeScript prevents mixing IDs
|
|
149
|
+
function getUser(id: UserId): User { ... }
|
|
150
|
+
function getOrder(id: OrderId): Order { ... }
|
|
151
|
+
|
|
152
|
+
const userId = createUserId('usr_123');
|
|
153
|
+
const orderId = createOrderId('ord_456');
|
|
154
|
+
|
|
155
|
+
getUser(userId); // OK
|
|
156
|
+
getUser(orderId); // TypeScript ERROR - type mismatch
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Discriminated Unions for State
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// CORRECT - use discriminated unions
|
|
163
|
+
type RequestState<T> =
|
|
164
|
+
| { status: 'idle' }
|
|
165
|
+
| { status: 'loading' }
|
|
166
|
+
| { status: 'success'; data: T }
|
|
167
|
+
| { status: 'error'; error: Error };
|
|
168
|
+
|
|
169
|
+
function handleState(state: RequestState<User>) {
|
|
170
|
+
switch (state.status) {
|
|
171
|
+
case 'idle':
|
|
172
|
+
return null;
|
|
173
|
+
case 'loading':
|
|
174
|
+
return <Spinner />;
|
|
175
|
+
case 'success':
|
|
176
|
+
return <UserCard user={state.data} />; // TypeScript knows data exists
|
|
177
|
+
case 'error':
|
|
178
|
+
return <ErrorMessage error={state.error} />; // TypeScript knows error exists
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Result Type for Error Handling
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// Define Result type
|
|
187
|
+
type Result<T, E = Error> =
|
|
188
|
+
| { success: true; data: T }
|
|
189
|
+
| { success: false; error: E };
|
|
190
|
+
|
|
191
|
+
// Usage
|
|
192
|
+
async function createUser(input: CreateUserInput): Promise<Result<User, ValidationError>> {
|
|
193
|
+
const validation = userSchema.safeParse(input);
|
|
194
|
+
if (!validation.success) {
|
|
195
|
+
return { success: false, error: new ValidationError(validation.error) };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const user = await db.user.create({ data: validation.data });
|
|
199
|
+
return { success: true, data: user };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Pattern matching approach
|
|
203
|
+
const result = await createUser(input);
|
|
204
|
+
if (result.success) {
|
|
205
|
+
console.log(result.data.id); // TypeScript knows data exists
|
|
206
|
+
} else {
|
|
207
|
+
console.error(result.error.message); // TypeScript knows error exists
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Zod Validation Patterns
|
|
214
|
+
|
|
215
|
+
### Schema Definition
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { z } from 'zod';
|
|
219
|
+
|
|
220
|
+
// Reusable primitives
|
|
221
|
+
const emailSchema = z.string().email();
|
|
222
|
+
const uuidSchema = z.string().uuid();
|
|
223
|
+
const moneySchema = z.number().positive().multipleOf(0.01);
|
|
224
|
+
|
|
225
|
+
// Compose schemas
|
|
226
|
+
const createUserSchema = z.object({
|
|
227
|
+
email: emailSchema,
|
|
228
|
+
name: z.string().min(1).max(100),
|
|
229
|
+
role: z.enum(['admin', 'user', 'guest']),
|
|
230
|
+
preferences: z.object({
|
|
231
|
+
theme: z.enum(['light', 'dark']).default('light'),
|
|
232
|
+
notifications: z.boolean().default(true),
|
|
233
|
+
}).optional(),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Infer TypeScript type from schema
|
|
237
|
+
type CreateUserInput = z.infer<typeof createUserSchema>;
|
|
238
|
+
|
|
239
|
+
// Runtime validation
|
|
240
|
+
function createUser(input: unknown): CreateUserInput {
|
|
241
|
+
return createUserSchema.parse(input); // Throws on invalid
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Safe parsing (returns Result-like)
|
|
245
|
+
function validateUser(input: unknown) {
|
|
246
|
+
const result = createUserSchema.safeParse(input);
|
|
247
|
+
if (!result.success) {
|
|
248
|
+
return { error: result.error.flatten() };
|
|
249
|
+
}
|
|
250
|
+
return { data: result.data };
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Schema Composition
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Base schemas
|
|
258
|
+
const timestampSchema = z.object({
|
|
259
|
+
createdAt: z.date(),
|
|
260
|
+
updatedAt: z.date(),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const identifiableSchema = z.object({
|
|
264
|
+
id: uuidSchema,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Compose for full entity
|
|
268
|
+
const userSchema = identifiableSchema
|
|
269
|
+
.merge(timestampSchema)
|
|
270
|
+
.extend({
|
|
271
|
+
email: emailSchema,
|
|
272
|
+
name: z.string(),
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Dependency Injection
|
|
279
|
+
|
|
280
|
+
### Using TSyringe
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { container, injectable, inject } from 'tsyringe';
|
|
284
|
+
|
|
285
|
+
// Define interface
|
|
286
|
+
interface UserRepository {
|
|
287
|
+
findById(id: string): Promise<User | null>;
|
|
288
|
+
save(user: User): Promise<void>;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Implement
|
|
292
|
+
@injectable()
|
|
293
|
+
class PostgresUserRepository implements UserRepository {
|
|
294
|
+
constructor(
|
|
295
|
+
@inject('Database') private db: Database
|
|
296
|
+
) {}
|
|
297
|
+
|
|
298
|
+
async findById(id: string): Promise<User | null> {
|
|
299
|
+
return this.db.user.findUnique({ where: { id } });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async save(user: User): Promise<void> {
|
|
303
|
+
await this.db.user.upsert({ where: { id: user.id }, ...user });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Service using repository
|
|
308
|
+
@injectable()
|
|
309
|
+
class UserService {
|
|
310
|
+
constructor(
|
|
311
|
+
@inject('UserRepository') private repo: UserRepository
|
|
312
|
+
) {}
|
|
313
|
+
|
|
314
|
+
async getUser(id: string): Promise<User> {
|
|
315
|
+
const user = await this.repo.findById(id);
|
|
316
|
+
if (!user) throw new NotFoundError('User not found');
|
|
317
|
+
return user;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Register in container
|
|
322
|
+
container.register('Database', { useClass: PrismaDatabase });
|
|
323
|
+
container.register('UserRepository', { useClass: PostgresUserRepository });
|
|
324
|
+
|
|
325
|
+
// Resolve
|
|
326
|
+
const userService = container.resolve(UserService);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## AsyncLocalStorage for Context
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
335
|
+
|
|
336
|
+
// Define context type
|
|
337
|
+
interface RequestContext {
|
|
338
|
+
requestId: string;
|
|
339
|
+
userId?: string;
|
|
340
|
+
tenantId?: string;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Create storage
|
|
344
|
+
const asyncLocalStorage = new AsyncLocalStorage<RequestContext>();
|
|
345
|
+
|
|
346
|
+
// Get current context
|
|
347
|
+
export function getContext(): RequestContext {
|
|
348
|
+
const ctx = asyncLocalStorage.getStore();
|
|
349
|
+
if (!ctx) throw new Error('No context available');
|
|
350
|
+
return ctx;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Middleware to set context
|
|
354
|
+
export function contextMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
355
|
+
const context: RequestContext = {
|
|
356
|
+
requestId: req.headers['x-request-id'] as string || crypto.randomUUID(),
|
|
357
|
+
userId: req.user?.id,
|
|
358
|
+
tenantId: req.headers['x-tenant-id'] as string,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
asyncLocalStorage.run(context, () => next());
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Usage anywhere in call chain
|
|
365
|
+
async function processOrder(orderId: string) {
|
|
366
|
+
const { tenantId, userId } = getContext();
|
|
367
|
+
logger.info('Processing order', { orderId, tenantId, userId });
|
|
368
|
+
// ...
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Testing
|
|
375
|
+
|
|
376
|
+
### Type-Safe Mocks
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { vi, describe, it, expect } from 'vitest';
|
|
380
|
+
|
|
381
|
+
// Create typed mock
|
|
382
|
+
const mockUserRepository: jest.Mocked<UserRepository> = {
|
|
383
|
+
findById: vi.fn(),
|
|
384
|
+
save: vi.fn(),
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
describe('UserService', () => {
|
|
388
|
+
it('returns user when found', async () => {
|
|
389
|
+
// Arrange
|
|
390
|
+
const user: User = { id: 'usr_123', name: 'John', email: 'john@example.com' };
|
|
391
|
+
mockUserRepository.findById.mockResolvedValue(user);
|
|
392
|
+
|
|
393
|
+
const service = new UserService(mockUserRepository);
|
|
394
|
+
|
|
395
|
+
// Act
|
|
396
|
+
const result = await service.getUser('usr_123');
|
|
397
|
+
|
|
398
|
+
// Assert
|
|
399
|
+
expect(result).toEqual(user);
|
|
400
|
+
expect(mockUserRepository.findById).toHaveBeenCalledWith('usr_123');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('throws NotFoundError when user not found', async () => {
|
|
404
|
+
// Arrange
|
|
405
|
+
mockUserRepository.findById.mockResolvedValue(null);
|
|
406
|
+
|
|
407
|
+
const service = new UserService(mockUserRepository);
|
|
408
|
+
|
|
409
|
+
// Act & Assert
|
|
410
|
+
await expect(service.getUser('usr_999')).rejects.toThrow(NotFoundError);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Type-Safe Fixtures
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// fixtures/user.ts
|
|
419
|
+
import { faker } from '@faker-js/faker';
|
|
420
|
+
|
|
421
|
+
export function createUserFixture(overrides: Partial<User> = {}): User {
|
|
422
|
+
return {
|
|
423
|
+
id: `usr_${faker.string.uuid()}`,
|
|
424
|
+
name: faker.person.fullName(),
|
|
425
|
+
email: faker.internet.email(),
|
|
426
|
+
createdAt: faker.date.past(),
|
|
427
|
+
updatedAt: new Date(),
|
|
428
|
+
...overrides,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Usage in tests
|
|
433
|
+
const user = createUserFixture({ name: 'Test User' });
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Edge Case Coverage (MANDATORY)
|
|
437
|
+
|
|
438
|
+
**Every acceptance criterion MUST have edge case tests beyond the happy path.**
|
|
439
|
+
|
|
440
|
+
| AC Type | Required Edge Cases | Minimum Count |
|
|
441
|
+
|---------|---------------------|---------------|
|
|
442
|
+
| Input validation | null, undefined, empty string, boundary values, invalid format, special chars | 3+ |
|
|
443
|
+
| CRUD operations | not found, duplicate, concurrent access, large payload | 3+ |
|
|
444
|
+
| Business logic | zero, negative, overflow, boundary conditions, invalid state | 3+ |
|
|
445
|
+
| Error handling | timeout, connection refused, invalid response, retry exhausted | 2+ |
|
|
446
|
+
| Authentication | expired token, invalid token, missing token, revoked token | 2+ |
|
|
447
|
+
|
|
448
|
+
**Edge Case Test Pattern:**
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
describe('UserService', () => {
|
|
452
|
+
describe('createUser', () => {
|
|
453
|
+
// Happy path
|
|
454
|
+
it('creates user with valid input', async () => {
|
|
455
|
+
const result = await service.createUser(validInput);
|
|
456
|
+
expect(result.id).toBeDefined();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Edge cases (MANDATORY - minimum 3)
|
|
460
|
+
it('throws ValidationError for null input', async () => {
|
|
461
|
+
await expect(service.createUser(null as any)).rejects.toThrow(ValidationError);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('throws ValidationError for empty email', async () => {
|
|
465
|
+
await expect(service.createUser({ ...validInput, email: '' })).rejects.toThrow(ValidationError);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('throws ValidationError for invalid email format', async () => {
|
|
469
|
+
await expect(service.createUser({ ...validInput, email: 'invalid' })).rejects.toThrow(ValidationError);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('throws ValidationError for email exceeding max length', async () => {
|
|
473
|
+
const longEmail = 'a'.repeat(256) + '@test.com';
|
|
474
|
+
await expect(service.createUser({ ...validInput, email: longEmail })).rejects.toThrow(ValidationError);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('throws DuplicateError for existing email', async () => {
|
|
478
|
+
mockRepo.findByEmail.mockResolvedValue(existingUser);
|
|
479
|
+
await expect(service.createUser(validInput)).rejects.toThrow(DuplicateError);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
**Anti-Pattern (FORBIDDEN):**
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
// ❌ WRONG: Only happy path
|
|
489
|
+
describe('UserService', () => {
|
|
490
|
+
it('creates user', async () => {
|
|
491
|
+
const result = await service.createUser(validInput);
|
|
492
|
+
expect(result).toBeDefined(); // No edge cases = incomplete test
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Error Handling
|
|
500
|
+
|
|
501
|
+
### Custom Error Classes
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
// Base application error
|
|
505
|
+
export class AppError extends Error {
|
|
506
|
+
constructor(
|
|
507
|
+
message: string,
|
|
508
|
+
public readonly code: string,
|
|
509
|
+
public readonly statusCode: number = 500,
|
|
510
|
+
public readonly details?: Record<string, unknown>
|
|
511
|
+
) {
|
|
512
|
+
super(message);
|
|
513
|
+
this.name = this.constructor.name;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
toJSON() {
|
|
517
|
+
return {
|
|
518
|
+
error: {
|
|
519
|
+
code: this.code,
|
|
520
|
+
message: this.message,
|
|
521
|
+
details: this.details,
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Specific errors
|
|
528
|
+
export class NotFoundError extends AppError {
|
|
529
|
+
constructor(resource: string) {
|
|
530
|
+
super(`${resource} not found`, 'NOT_FOUND', 404);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export class ValidationError extends AppError {
|
|
535
|
+
constructor(errors: z.ZodError) {
|
|
536
|
+
super('Validation failed', 'VALIDATION_ERROR', 400, {
|
|
537
|
+
fields: errors.flatten().fieldErrors,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export class UnauthorizedError extends AppError {
|
|
543
|
+
constructor(message = 'Unauthorized') {
|
|
544
|
+
super(message, 'UNAUTHORIZED', 401);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Function Design (MANDATORY)
|
|
552
|
+
|
|
553
|
+
**Single Responsibility Principle (SRP):** Each function MUST have exactly ONE responsibility.
|
|
554
|
+
|
|
555
|
+
### Rules
|
|
556
|
+
|
|
557
|
+
| Rule | Description |
|
|
558
|
+
|------|-------------|
|
|
559
|
+
| **One responsibility per function** | A function should do ONE thing and do it well |
|
|
560
|
+
| **Max 20-30 lines** | If longer, break into smaller functions |
|
|
561
|
+
| **One level of abstraction** | Don't mix high-level and low-level operations |
|
|
562
|
+
| **Descriptive names** | Function name should describe its single responsibility |
|
|
563
|
+
|
|
564
|
+
### Examples
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
// ❌ BAD - Multiple responsibilities
|
|
568
|
+
async function processOrder(order: Order): Promise<void> {
|
|
569
|
+
// Validate order
|
|
570
|
+
if (!order.items?.length) {
|
|
571
|
+
throw new Error('no items');
|
|
572
|
+
}
|
|
573
|
+
// Calculate total
|
|
574
|
+
let total = 0;
|
|
575
|
+
for (const item of order.items) {
|
|
576
|
+
total += item.price * item.quantity;
|
|
577
|
+
}
|
|
578
|
+
// Apply discount
|
|
579
|
+
if (order.couponCode) {
|
|
580
|
+
total = total * 0.9;
|
|
581
|
+
}
|
|
582
|
+
// Save to database
|
|
583
|
+
await db.orders.save(order);
|
|
584
|
+
// Send email
|
|
585
|
+
await sendEmail(order.customerEmail, 'Order confirmed');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ✅ GOOD - Single responsibility per function
|
|
589
|
+
async function processOrder(order: Order): Promise<void> {
|
|
590
|
+
validateOrder(order);
|
|
591
|
+
const total = calculateTotal(order.items);
|
|
592
|
+
const finalTotal = applyDiscount(total, order.couponCode);
|
|
593
|
+
await saveOrder(order, finalTotal);
|
|
594
|
+
await notifyCustomer(order.customerEmail);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function validateOrder(order: Order): void {
|
|
598
|
+
if (!order.items?.length) {
|
|
599
|
+
throw new ValidationError('Order must have items');
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function calculateTotal(items: OrderItem[]): number {
|
|
604
|
+
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function applyDiscount(total: number, couponCode?: string): number {
|
|
608
|
+
return couponCode ? total * 0.9 : total;
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Signs a Function Has Multiple Responsibilities
|
|
613
|
+
|
|
614
|
+
| Sign | Action |
|
|
615
|
+
|------|--------|
|
|
616
|
+
| Multiple `// section` comments | Split at comment boundaries |
|
|
617
|
+
| "and" in function name | Split into separate functions |
|
|
618
|
+
| More than 3 parameters | Consider parameter object or splitting |
|
|
619
|
+
| Nested conditionals > 2 levels | Extract inner logic to functions |
|
|
620
|
+
| Function does validation and processing | Separate validation function |
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## Naming Conventions
|
|
625
|
+
|
|
626
|
+
| Element | Convention | Example |
|
|
627
|
+
|---------|------------|---------|
|
|
628
|
+
| Files | kebab-case | `user-service.ts` |
|
|
629
|
+
| Interfaces | PascalCase | `UserRepository` |
|
|
630
|
+
| Types | PascalCase | `CreateUserInput` |
|
|
631
|
+
| Functions | camelCase | `createUser` |
|
|
632
|
+
| Constants | UPPER_SNAKE | `MAX_RETRY_COUNT` |
|
|
633
|
+
| Enums | PascalCase + UPPER_SNAKE values | `UserRole.ADMIN` |
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## Directory Structure
|
|
638
|
+
|
|
639
|
+
The directory structure follows the **Lerian pattern** - a simplified hexagonal architecture without explicit DDD folders.
|
|
640
|
+
|
|
641
|
+
```
|
|
642
|
+
/src
|
|
643
|
+
/bootstrap # Application initialization
|
|
644
|
+
config.ts
|
|
645
|
+
server.ts
|
|
646
|
+
service.ts
|
|
647
|
+
/services # Business logic
|
|
648
|
+
/command # Write operations (use cases)
|
|
649
|
+
/query # Read operations (use cases)
|
|
650
|
+
/adapters # Infrastructure implementations
|
|
651
|
+
/http/in # HTTP handlers + routes
|
|
652
|
+
/grpc/in # gRPC handlers (if needed)
|
|
653
|
+
/postgres # PostgreSQL repositories
|
|
654
|
+
/mongodb # MongoDB repositories
|
|
655
|
+
/redis # Redis repositories
|
|
656
|
+
/rabbitmq # RabbitMQ producers/consumers
|
|
657
|
+
/lib # Utilities
|
|
658
|
+
db.ts
|
|
659
|
+
logger.ts
|
|
660
|
+
/types # Shared types and models
|
|
661
|
+
index.ts
|
|
662
|
+
/tests
|
|
663
|
+
/unit
|
|
664
|
+
/integration
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**Key differences from traditional DDD:**
|
|
668
|
+
- **No `/src/domain` folder** - Business entities live in `/src/types` or within service files
|
|
669
|
+
- **Services are the core** - `/src/services` contains all business logic (command/query pattern)
|
|
670
|
+
- **Adapters are flat** - Database repositories are organized by technology, not by domain
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## RabbitMQ Worker Pattern
|
|
675
|
+
|
|
676
|
+
When the application includes async processing (API+Worker or Worker Only), follow this pattern.
|
|
677
|
+
|
|
678
|
+
### Application Types
|
|
679
|
+
|
|
680
|
+
| Type | Characteristics | Components |
|
|
681
|
+
|------|----------------|------------|
|
|
682
|
+
| **API Only** | HTTP endpoints, no async processing | Handlers, Services, Repositories |
|
|
683
|
+
| **API + Worker** | HTTP endpoints + async message processing | All above + Consumers, Producers |
|
|
684
|
+
| **Worker Only** | No HTTP, only message processing | Consumers, Services, Repositories |
|
|
685
|
+
|
|
686
|
+
### Architecture Overview
|
|
687
|
+
|
|
688
|
+
```text
|
|
689
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
690
|
+
│ Service Bootstrap │
|
|
691
|
+
│ ├── HTTP Server (Express/Fastify) ← API endpoints │
|
|
692
|
+
│ ├── RabbitMQ Consumer ← Event-driven workers │
|
|
693
|
+
│ └── Redis Consumer (optional) ← Scheduled polling │
|
|
694
|
+
└─────────────────────────────────────────────────────────────┘
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### Core Types
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
// Handler function signature
|
|
701
|
+
type QueueHandlerFunc = (ctx: Context, body: Buffer) => Promise<void>;
|
|
702
|
+
|
|
703
|
+
// Consumer configuration
|
|
704
|
+
interface ConsumerConfig {
|
|
705
|
+
connection: RabbitMQConnection;
|
|
706
|
+
routes: Map<string, QueueHandlerFunc>;
|
|
707
|
+
numberOfWorkers: number; // Workers per queue (default: 5)
|
|
708
|
+
prefetchCount: number; // QoS prefetch (default: 10)
|
|
709
|
+
logger: Logger;
|
|
710
|
+
telemetry: Telemetry;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Context for handlers
|
|
714
|
+
interface Context {
|
|
715
|
+
requestId: string;
|
|
716
|
+
logger: Logger;
|
|
717
|
+
span: Span;
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### Worker Configuration
|
|
722
|
+
|
|
723
|
+
| Config | Default | Purpose |
|
|
724
|
+
|--------|---------|---------|
|
|
725
|
+
| `RABBITMQ_NUMBERS_OF_WORKERS` | 5 | Concurrent workers per queue |
|
|
726
|
+
| `RABBITMQ_NUMBERS_OF_PREFETCH` | 10 | Messages buffered per worker |
|
|
727
|
+
| `RABBITMQ_CONSUMER_USER` | - | Separate credentials for consumer |
|
|
728
|
+
| `RABBITMQ_{QUEUE}_QUEUE` | - | Queue name per handler |
|
|
729
|
+
|
|
730
|
+
**Formula:** `Total buffered = Workers × Prefetch` (e.g., 5 × 10 = 50 messages)
|
|
731
|
+
|
|
732
|
+
### Handler Registration
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
// Register handlers per queue
|
|
736
|
+
class MultiQueueConsumer {
|
|
737
|
+
registerRoutes(routes: ConsumerRoutes): void {
|
|
738
|
+
routes.register(
|
|
739
|
+
process.env.RABBITMQ_BALANCE_CREATE_QUEUE!,
|
|
740
|
+
this.handleBalanceCreate.bind(this)
|
|
741
|
+
);
|
|
742
|
+
routes.register(
|
|
743
|
+
process.env.RABBITMQ_TRANSACTION_QUEUE!,
|
|
744
|
+
this.handleTransaction.bind(this)
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### Handler Implementation
|
|
751
|
+
|
|
752
|
+
```typescript
|
|
753
|
+
async handleBalanceCreate(ctx: Context, body: Buffer): Promise<void> {
|
|
754
|
+
// 1. Parse and validate message
|
|
755
|
+
const parsed = queueMessageSchema.safeParse(JSON.parse(body.toString()));
|
|
756
|
+
if (!parsed.success) {
|
|
757
|
+
ctx.logger.error('Invalid message format', { error: parsed.error });
|
|
758
|
+
throw new Error(`Invalid message: ${parsed.error.message}`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// 2. Execute business logic
|
|
762
|
+
const result = await this.useCase.createBalance(ctx, parsed.data);
|
|
763
|
+
if (!result.success) {
|
|
764
|
+
throw result.error;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// 3. Success → Ack automatically (by returning without error)
|
|
768
|
+
}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
### Message Acknowledgment
|
|
772
|
+
|
|
773
|
+
| Result | Action | Effect |
|
|
774
|
+
|--------|--------|--------|
|
|
775
|
+
| Resolves | `msg.ack()` | Message removed from queue |
|
|
776
|
+
| Rejects/Throws | `msg.nack(false, true)` | Message requeued |
|
|
777
|
+
|
|
778
|
+
### Worker Lifecycle
|
|
779
|
+
|
|
780
|
+
```text
|
|
781
|
+
runConsumers()
|
|
782
|
+
├── For each registered queue:
|
|
783
|
+
│ ├── ensureChannel() with exponential backoff
|
|
784
|
+
│ ├── Set QoS (prefetch)
|
|
785
|
+
│ ├── Start consume()
|
|
786
|
+
│ └── Process messages with concurrency limit
|
|
787
|
+
|
|
788
|
+
processMessage():
|
|
789
|
+
├── Extract/generate TraceID from headers
|
|
790
|
+
├── Create context with requestId
|
|
791
|
+
├── Start OpenTelemetry span
|
|
792
|
+
├── Call handler(ctx, msg.content)
|
|
793
|
+
├── On success: msg.ack()
|
|
794
|
+
└── On error: log + msg.nack(false, true)
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### Exponential Backoff with Jitter
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
const BACKOFF_CONFIG = {
|
|
801
|
+
maxRetries: 5,
|
|
802
|
+
initialBackoff: 500, // ms
|
|
803
|
+
maxBackoff: 10_000, // ms
|
|
804
|
+
backoffFactor: 2.0,
|
|
805
|
+
} as const;
|
|
806
|
+
|
|
807
|
+
function fullJitter(baseDelay: number): number {
|
|
808
|
+
const jitter = Math.random() * baseDelay;
|
|
809
|
+
return Math.min(jitter, BACKOFF_CONFIG.maxBackoff);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function nextBackoff(current: number): number {
|
|
813
|
+
const next = current * BACKOFF_CONFIG.backoffFactor;
|
|
814
|
+
return Math.min(next, BACKOFF_CONFIG.maxBackoff);
|
|
815
|
+
}
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### Producer Implementation
|
|
819
|
+
|
|
820
|
+
```typescript
|
|
821
|
+
class ProducerRepository {
|
|
822
|
+
async publish(
|
|
823
|
+
exchange: string,
|
|
824
|
+
routingKey: string,
|
|
825
|
+
message: unknown,
|
|
826
|
+
ctx: Context
|
|
827
|
+
): Promise<void> {
|
|
828
|
+
await this.ensureChannel();
|
|
829
|
+
|
|
830
|
+
const headers = {
|
|
831
|
+
'x-request-id': ctx.requestId,
|
|
832
|
+
...injectTraceHeaders(ctx.span),
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
this.channel.publish(
|
|
836
|
+
exchange,
|
|
837
|
+
routingKey,
|
|
838
|
+
Buffer.from(JSON.stringify(message)),
|
|
839
|
+
{
|
|
840
|
+
contentType: 'application/json',
|
|
841
|
+
persistent: true,
|
|
842
|
+
headers,
|
|
843
|
+
}
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
### Message Schema with Zod
|
|
850
|
+
|
|
851
|
+
```typescript
|
|
852
|
+
const queueDataSchema = z.object({
|
|
853
|
+
id: z.string().uuid(),
|
|
854
|
+
value: z.unknown(),
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const queueMessageSchema = z.object({
|
|
858
|
+
organizationId: z.string().uuid(),
|
|
859
|
+
ledgerId: z.string().uuid(),
|
|
860
|
+
auditId: z.string().uuid(),
|
|
861
|
+
data: z.array(queueDataSchema),
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
type QueueMessage = z.infer<typeof queueMessageSchema>;
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### Service Bootstrap (API + Worker)
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
class Service {
|
|
871
|
+
constructor(
|
|
872
|
+
private readonly server: HttpServer,
|
|
873
|
+
private readonly consumer: MultiQueueConsumer,
|
|
874
|
+
private readonly logger: Logger,
|
|
875
|
+
) {}
|
|
876
|
+
|
|
877
|
+
async run(): Promise<void> {
|
|
878
|
+
// Run all components concurrently
|
|
879
|
+
await Promise.all([
|
|
880
|
+
this.server.listen(),
|
|
881
|
+
this.consumer.start(),
|
|
882
|
+
]);
|
|
883
|
+
|
|
884
|
+
// Graceful shutdown
|
|
885
|
+
process.on('SIGTERM', async () => {
|
|
886
|
+
this.logger.info('Shutting down...');
|
|
887
|
+
await this.consumer.stop();
|
|
888
|
+
await this.server.close();
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
### Directory Structure for Workers
|
|
895
|
+
|
|
896
|
+
```text
|
|
897
|
+
/src
|
|
898
|
+
/infrastructure
|
|
899
|
+
/rabbitmq
|
|
900
|
+
consumer.ts # ConsumerRoutes, worker pool
|
|
901
|
+
producer.ts # ProducerRepository
|
|
902
|
+
connection.ts # Connection management
|
|
903
|
+
/bootstrap
|
|
904
|
+
rabbitmq-server.ts # MultiQueueConsumer, handler registration
|
|
905
|
+
service.ts # Service orchestration
|
|
906
|
+
/lib
|
|
907
|
+
backoff.ts # Backoff utilities
|
|
908
|
+
/types
|
|
909
|
+
queue.ts # Message schemas
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
### Worker Checklist
|
|
913
|
+
|
|
914
|
+
- [ ] Handlers are idempotent (safe to process duplicates)
|
|
915
|
+
- [ ] Manual Ack enabled (`noAck: false`)
|
|
916
|
+
- [ ] Error handling throws error (triggers Nack)
|
|
917
|
+
- [ ] Context propagation with requestId
|
|
918
|
+
- [ ] OpenTelemetry spans for tracing
|
|
919
|
+
- [ ] Exponential backoff for connection recovery
|
|
920
|
+
- [ ] Graceful shutdown with proper cleanup
|
|
921
|
+
- [ ] Separate credentials for consumer vs producer
|
|
922
|
+
- [ ] Zod validation for all message payloads
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
## Always-Valid Domain Model (MANDATORY)
|
|
927
|
+
|
|
928
|
+
**HARD GATE:** All domain entities MUST use the Always-Valid Domain Model pattern. Anemic models (plain objects without validation) are FORBIDDEN.
|
|
929
|
+
|
|
930
|
+
### Why This Pattern Is Mandatory
|
|
931
|
+
|
|
932
|
+
| Problem with Anemic Models | Impact |
|
|
933
|
+
|---------------------------|--------|
|
|
934
|
+
| Objects can exist in invalid state | Bugs propagate through system |
|
|
935
|
+
| Validation scattered across codebase | Duplication, inconsistency |
|
|
936
|
+
| Business rules not enforced at creation | Invalid data reaches database |
|
|
937
|
+
| No single source of truth for validity | Every consumer must re-validate |
|
|
938
|
+
|
|
939
|
+
### The Pattern
|
|
940
|
+
|
|
941
|
+
**Core Principle:** An entity can NEVER exist in an invalid state. Validation happens in the factory, not later.
|
|
942
|
+
|
|
943
|
+
```typescript
|
|
944
|
+
// ✅ CORRECT: Always-Valid Domain Model
|
|
945
|
+
class Rule {
|
|
946
|
+
private constructor(
|
|
947
|
+
private readonly _id: string,
|
|
948
|
+
private readonly _name: string,
|
|
949
|
+
private readonly _expression: string,
|
|
950
|
+
private readonly _createdAt: Date,
|
|
951
|
+
) {}
|
|
952
|
+
|
|
953
|
+
// Factory method MUST validate and return Result
|
|
954
|
+
static create(name: string, expression: string): Result<Rule, ValidationError> {
|
|
955
|
+
// Validation at construction time
|
|
956
|
+
if (!name || name.trim().length === 0) {
|
|
957
|
+
return err(new ValidationError('name is required'));
|
|
958
|
+
}
|
|
959
|
+
if (name.length > 255) {
|
|
960
|
+
return err(new ValidationError('name exceeds 255 characters'));
|
|
961
|
+
}
|
|
962
|
+
if (!isValidExpression(expression)) {
|
|
963
|
+
return err(new ValidationError('invalid expression syntax'));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return ok(new Rule(
|
|
967
|
+
crypto.randomUUID(),
|
|
968
|
+
name.trim(),
|
|
969
|
+
expression,
|
|
970
|
+
new Date(),
|
|
971
|
+
));
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Getters expose immutable data
|
|
975
|
+
get id(): string { return this._id; }
|
|
976
|
+
get name(): string { return this._name; }
|
|
977
|
+
get expression(): string { return this._expression; }
|
|
978
|
+
}
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
```typescript
|
|
982
|
+
// ❌ FORBIDDEN: Anemic Model (validation elsewhere)
|
|
983
|
+
interface Rule {
|
|
984
|
+
id: string;
|
|
985
|
+
name: string; // Can be empty - invalid!
|
|
986
|
+
expression: string; // Can be invalid - no validation!
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// ❌ FORBIDDEN: Factory without validation
|
|
990
|
+
function createRule(name: string, expression: string): Rule {
|
|
991
|
+
return {
|
|
992
|
+
id: crypto.randomUUID(),
|
|
993
|
+
name, // No validation!
|
|
994
|
+
expression, // No validation!
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
### Requirements
|
|
1000
|
+
|
|
1001
|
+
| Requirement | Description |
|
|
1002
|
+
|-------------|-------------|
|
|
1003
|
+
| **Factory returns Result** | `Entity.create(...): Result<Entity, Error>` - MUST return error if invalid |
|
|
1004
|
+
| **Private constructor** | Prevent direct instantiation with `new` |
|
|
1005
|
+
| **Readonly properties** | Use `readonly` or getters to prevent mutation |
|
|
1006
|
+
| **No Setters** | Mutation through domain methods that validate |
|
|
1007
|
+
| **Invariants enforced** | Business rules validated at construction |
|
|
1008
|
+
|
|
1009
|
+
### Mutation Pattern
|
|
1010
|
+
|
|
1011
|
+
When entities need to change state, use domain methods that validate:
|
|
1012
|
+
|
|
1013
|
+
```typescript
|
|
1014
|
+
// ✅ CORRECT: Mutation with validation
|
|
1015
|
+
class Rule {
|
|
1016
|
+
// ...
|
|
1017
|
+
|
|
1018
|
+
updateExpression(newExpression: string): Result<void, ValidationError> {
|
|
1019
|
+
if (!isValidExpression(newExpression)) {
|
|
1020
|
+
return err(new ValidationError('invalid expression syntax'));
|
|
1021
|
+
}
|
|
1022
|
+
// TypeScript: use Object.assign or create new instance for immutability
|
|
1023
|
+
Object.assign(this, { _expression: newExpression });
|
|
1024
|
+
return ok(undefined);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// ❌ FORBIDDEN: Direct property assignment
|
|
1029
|
+
rule.expression = 'invalid!!!'; // Compilation error (readonly)
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
### Reconstruction from Database
|
|
1033
|
+
|
|
1034
|
+
When loading from database, use a separate reconstruction method:
|
|
1035
|
+
|
|
1036
|
+
```typescript
|
|
1037
|
+
// For repository use ONLY - reconstructs from trusted storage
|
|
1038
|
+
static reconstruct(
|
|
1039
|
+
id: string,
|
|
1040
|
+
name: string,
|
|
1041
|
+
expression: string,
|
|
1042
|
+
createdAt: Date,
|
|
1043
|
+
): Rule {
|
|
1044
|
+
// Skip validation - data is from trusted storage
|
|
1045
|
+
return new Rule(id, name, expression, createdAt);
|
|
1046
|
+
}
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
**Note:** `reconstruct` methods skip validation because data is from trusted storage (already validated at creation).
|
|
1050
|
+
|
|
1051
|
+
### Integration with HTTP Layer
|
|
1052
|
+
|
|
1053
|
+
HTTP handlers still use Zod for input validation, but MUST create domain entities via factories:
|
|
1054
|
+
|
|
1055
|
+
```typescript
|
|
1056
|
+
// Zod schema - validation at boundary
|
|
1057
|
+
const createRuleSchema = z.object({
|
|
1058
|
+
name: z.string().min(1).max(255),
|
|
1059
|
+
expression: z.string().min(1),
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// Handler creates domain entity
|
|
1063
|
+
async function createRule(req: Request): Promise<Response> {
|
|
1064
|
+
// Boundary validation
|
|
1065
|
+
const parsed = createRuleSchema.safeParse(req.body);
|
|
1066
|
+
if (!parsed.success) {
|
|
1067
|
+
return errorResponse(parsed.error);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Domain entity creation - additional business validation
|
|
1071
|
+
const ruleResult = Rule.create(parsed.data.name, parsed.data.expression);
|
|
1072
|
+
if (ruleResult.isErr()) {
|
|
1073
|
+
return errorResponse(ruleResult.error);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// ...
|
|
1077
|
+
}
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
### Result Type Pattern
|
|
1081
|
+
|
|
1082
|
+
Use a Result type for operations that can fail:
|
|
1083
|
+
|
|
1084
|
+
```typescript
|
|
1085
|
+
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
|
|
1086
|
+
|
|
1087
|
+
function ok<T>(value: T): Result<T, never> {
|
|
1088
|
+
return { ok: true, value };
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function err<E>(error: E): Result<never, E> {
|
|
1092
|
+
return { ok: false, error };
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Usage
|
|
1096
|
+
const result = Rule.create(name, expression);
|
|
1097
|
+
if (result.ok) {
|
|
1098
|
+
const rule = result.value;
|
|
1099
|
+
} else {
|
|
1100
|
+
const error = result.error;
|
|
1101
|
+
}
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
### Anti-Rationalization Table
|
|
1105
|
+
|
|
1106
|
+
| Rationalization | Why It's WRONG | Required Action |
|
|
1107
|
+
|-----------------|----------------|-----------------|
|
|
1108
|
+
| "Zod validation at boundary is enough" | Boundary validation is for input format. Domain validation is for business rules. | **Use both: Zod validation + factory validation** |
|
|
1109
|
+
| "Adds boilerplate" | Invalid objects cause more work debugging than factories. | **Write the factory. It's an investment.** |
|
|
1110
|
+
| "We trust our code" | Every consumer must remember to validate. Humans forget. | **Enforce at construction. Forget-proof.** |
|
|
1111
|
+
| "Performance overhead" | Validation once at creation vs checking everywhere. | **Single validation is MORE efficient** |
|
|
1112
|
+
| "Existing code doesn't do this" | Technical debt. Refactor when touching the code. | **New code MUST follow. Refactor gradually.** |
|
|
1113
|
+
| "Plain interfaces are fine for DTOs" | DTOs are fine as plain objects. Domain entities are NOT. | **Distinguish DTO from Domain Entity** |
|
|
1114
|
+
|
|
1115
|
+
### Checklist
|
|
1116
|
+
|
|
1117
|
+
- [ ] All domain entities use `private constructor` + `static create()` factory
|
|
1118
|
+
- [ ] Factories return `Result<Entity, Error>` - never throw
|
|
1119
|
+
- [ ] Properties are `readonly` or accessed via getters
|
|
1120
|
+
- [ ] Mutation through validated methods only
|
|
1121
|
+
- [ ] Reconstruct methods for database loading
|
|
1122
|
+
- [ ] No direct object instantiation outside factories
|
|
1123
|
+
|
|
1124
|
+
---
|
|
1125
|
+
|
|
1126
|
+
## Checklist
|
|
1127
|
+
|
|
1128
|
+
Before submitting TypeScript code, verify:
|
|
1129
|
+
|
|
1130
|
+
### Type Safety
|
|
1131
|
+
- [ ] No `any` types (use `unknown` with narrowing)
|
|
1132
|
+
- [ ] Strict mode enabled in tsconfig.json
|
|
1133
|
+
- [ ] Zod validation for all external input
|
|
1134
|
+
- [ ] Branded types for IDs
|
|
1135
|
+
- [ ] Discriminated unions for state machines
|
|
1136
|
+
- [ ] Type inference used where possible (avoid redundant annotations)
|
|
1137
|
+
- [ ] No `@ts-ignore` or `@ts-expect-error` without explanation
|
|
1138
|
+
|
|
1139
|
+
### Error Handling
|
|
1140
|
+
- [ ] Error classes extend base AppError
|
|
1141
|
+
- [ ] All async functions have proper error handling
|
|
1142
|
+
- [ ] Result type used for operations that can fail
|
|
1143
|
+
|
|
1144
|
+
### DDD (if enabled)
|
|
1145
|
+
- [ ] Entities have identity comparison (`equals` method)
|
|
1146
|
+
- [ ] Value Objects are immutable (private constructor, factory methods)
|
|
1147
|
+
- [ ] Aggregates enforce invariants before state changes
|
|
1148
|
+
- [ ] Domain Events emitted for significant state changes
|
|
1149
|
+
- [ ] Repository interfaces defined in domain layer
|
|
1150
|
+
- [ ] No infrastructure dependencies in domain layer
|