@quanticjs/create-app 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/deps.d.ts +4 -0
  2. package/dist/deps.js +97 -0
  3. package/dist/deps.js.map +1 -0
  4. package/dist/generators/backend.d.ts +2 -0
  5. package/dist/generators/backend.js +20 -0
  6. package/dist/generators/backend.js.map +1 -0
  7. package/dist/generators/bff.d.ts +2 -0
  8. package/dist/generators/bff.js +9 -0
  9. package/dist/generators/bff.js.map +1 -0
  10. package/dist/generators/claude.d.ts +2 -0
  11. package/dist/generators/claude.js +45 -0
  12. package/dist/generators/claude.js.map +1 -0
  13. package/dist/generators/docker.d.ts +2 -0
  14. package/dist/generators/docker.js +10 -0
  15. package/dist/generators/docker.js.map +1 -0
  16. package/dist/generators/e2e.d.ts +2 -0
  17. package/dist/generators/e2e.js +8 -0
  18. package/dist/generators/e2e.js.map +1 -0
  19. package/dist/generators/frontend.d.ts +2 -0
  20. package/dist/generators/frontend.js +28 -0
  21. package/dist/generators/frontend.js.map +1 -0
  22. package/dist/generators/module.d.ts +2 -0
  23. package/dist/generators/module.js +35 -0
  24. package/dist/generators/module.js.map +1 -0
  25. package/dist/generators/root.d.ts +2 -0
  26. package/dist/generators/root.js +7 -0
  27. package/dist/generators/root.js.map +1 -0
  28. package/dist/generators/scripts.d.ts +2 -0
  29. package/dist/generators/scripts.js +10 -0
  30. package/dist/generators/scripts.js.map +1 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +40 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/prompts.d.ts +8 -0
  35. package/dist/prompts.js +53 -0
  36. package/dist/prompts.js.map +1 -0
  37. package/dist/scaffold.d.ts +2 -0
  38. package/dist/scaffold.js +79 -0
  39. package/dist/scaffold.js.map +1 -0
  40. package/dist/utils/exec.d.ts +2 -0
  41. package/dist/utils/exec.js +14 -0
  42. package/dist/utils/exec.js.map +1 -0
  43. package/dist/utils/template.d.ts +10 -0
  44. package/dist/utils/template.js +20 -0
  45. package/dist/utils/template.js.map +1 -0
  46. package/dist/utils/validate.d.ts +4 -0
  47. package/dist/utils/validate.js +27 -0
  48. package/dist/utils/validate.js.map +1 -0
  49. package/package.json +50 -0
  50. package/templates/backend/app.module.ts.ejs +61 -0
  51. package/templates/backend/bff.controller.ts.ejs +60 -0
  52. package/templates/backend/bff.module.ts.ejs +10 -0
  53. package/templates/backend/bff.service.ts.ejs +6 -0
  54. package/templates/backend/create-schema.migration.ts.ejs +15 -0
  55. package/templates/backend/data-source.ts.ejs +13 -0
  56. package/templates/backend/env.example.ejs +30 -0
  57. package/templates/backend/main.ts.ejs +43 -0
  58. package/templates/backend/module.ts.ejs +14 -0
  59. package/templates/backend/nest-cli.json.ejs +8 -0
  60. package/templates/backend/package.json.ejs +23 -0
  61. package/templates/backend/tsconfig.build.json.ejs +4 -0
  62. package/templates/backend/tsconfig.json.ejs +24 -0
  63. package/templates/claude/CLAUDE.md.ejs +86 -0
  64. package/templates/claude/hooks/auto-format.sh +22 -0
  65. package/templates/claude/hooks/check-secrets.sh +49 -0
  66. package/templates/claude/hooks/guard-destructive.sh +42 -0
  67. package/templates/claude/hooks/on-compaction.sh +29 -0
  68. package/templates/claude/mcp.json +10 -0
  69. package/templates/claude/rules/api-patterns.md +86 -0
  70. package/templates/claude/rules/auth-patterns.md +109 -0
  71. package/templates/claude/rules/backend-patterns.md +421 -0
  72. package/templates/claude/rules/database-patterns.md +96 -0
  73. package/templates/claude/rules/docker-patterns.md +86 -0
  74. package/templates/claude/rules/frontend-patterns.md +262 -0
  75. package/templates/claude/rules/observability-backend.md +132 -0
  76. package/templates/claude/rules/observability-frontend.md +49 -0
  77. package/templates/claude/rules/playwright-mcp.md +80 -0
  78. package/templates/claude/rules/resilience-ops.md +103 -0
  79. package/templates/claude/rules/testing-e2e-ui.md +190 -0
  80. package/templates/claude/rules/testing-patterns.md +94 -0
  81. package/templates/claude/rules/workflow-backend.md +64 -0
  82. package/templates/claude/rules/workflow-frontend.md +60 -0
  83. package/templates/claude/settings.json +68 -0
  84. package/templates/claude/skills/add-api-endpoint/SKILL.md +59 -0
  85. package/templates/claude/skills/add-auth-endpoint/SKILL.md +68 -0
  86. package/templates/claude/skills/add-entity/SKILL.md +56 -0
  87. package/templates/claude/skills/add-event/SKILL.md +127 -0
  88. package/templates/claude/skills/add-feature/SKILL.md +20 -0
  89. package/templates/claude/skills/add-frontend-page/SKILL.md +75 -0
  90. package/templates/claude/skills/add-handler/SKILL.md +105 -0
  91. package/templates/claude/skills/add-integration/SKILL.md +176 -0
  92. package/templates/claude/skills/add-migration/SKILL.md +20 -0
  93. package/templates/claude/skills/add-module/SKILL.md +89 -0
  94. package/templates/claude/skills/add-realtime/SKILL.md +119 -0
  95. package/templates/claude/skills/audit-rules/SKILL.md +120 -0
  96. package/templates/claude/skills/debugging/SKILL.md +105 -0
  97. package/templates/claude/skills/docker-dev/SKILL.md +86 -0
  98. package/templates/claude/skills/e2e-audit/SKILL.md +85 -0
  99. package/templates/claude/skills/e2e-full/SKILL.md +132 -0
  100. package/templates/claude/skills/e2e-scan/SKILL.md +171 -0
  101. package/templates/claude/skills/e2e-verify/SKILL.md +145 -0
  102. package/templates/claude/skills/fix-bug/SKILL.md +33 -0
  103. package/templates/claude/skills/implement-spec/SKILL.md +98 -0
  104. package/templates/claude/skills/review-code/SKILL.md +109 -0
  105. package/templates/claude/skills/review-spec/SKILL.md +216 -0
  106. package/templates/claude/skills/run-tests/SKILL.md +37 -0
  107. package/templates/claude/skills/specify/SKILL.md +87 -0
  108. package/templates/claude/skills/write-backend-tests/SKILL.md +182 -0
  109. package/templates/claude/skills/write-ui-tests/SKILL.md +118 -0
  110. package/templates/docker/Dockerfile.client.ejs +14 -0
  111. package/templates/docker/Dockerfile.ejs +28 -0
  112. package/templates/docker/docker-compose.test.yml.ejs +54 -0
  113. package/templates/docker/docker-compose.yml.ejs +76 -0
  114. package/templates/docker/nginx.conf.ejs +21 -0
  115. package/templates/frontend/App.tsx.ejs +64 -0
  116. package/templates/frontend/DashboardPage.tsx.ejs +37 -0
  117. package/templates/frontend/LoginPage.tsx.ejs +20 -0
  118. package/templates/frontend/NotFoundPage.tsx.ejs +15 -0
  119. package/templates/frontend/api-client.ts.ejs +15 -0
  120. package/templates/frontend/index.css.ejs +57 -0
  121. package/templates/frontend/index.html.ejs +13 -0
  122. package/templates/frontend/main.tsx.ejs +10 -0
  123. package/templates/frontend/package.json.ejs +16 -0
  124. package/templates/frontend/playwright.config.ts.ejs +20 -0
  125. package/templates/frontend/postcss.config.js.ejs +3 -0
  126. package/templates/frontend/smoke.spec.ts.ejs +37 -0
  127. package/templates/frontend/tailwind.config.ts.ejs +56 -0
  128. package/templates/frontend/tsconfig.json.ejs +25 -0
  129. package/templates/frontend/tsconfig.node.json.ejs +15 -0
  130. package/templates/frontend/utils.ts.ejs +6 -0
  131. package/templates/frontend/vite-env.d.ts.ejs +1 -0
  132. package/templates/frontend/vite.config.ts.ejs +20 -0
  133. package/templates/root/gitignore.ejs +9 -0
  134. package/templates/root/prettierrc.ejs +7 -0
  135. package/templates/scripts/init-db.sh.ejs +8 -0
  136. package/templates/scripts/save-auth-state.ts.ejs +24 -0
@@ -0,0 +1,59 @@
1
+ # Add API Endpoint
2
+
3
+ Wire a CQRS handler to an HTTP endpoint with DTO validation and a thin controller.
4
+
5
+ ## Usage
6
+ ```
7
+ /add-api-endpoint POST /project/items
8
+ /add-api-endpoint GET /identity/users/:id
9
+ ```
10
+
11
+ ## Steps
12
+ 1. **Create handler** — run `/add-handler` for the command/query + validator + handler
13
+ 2. **Create DTO** with class-validator decorators (controller-layer validation):
14
+ ```typescript
15
+ import { IsString, IsNotEmpty, MaxLength } from 'class-validator';
16
+ import { ApiProperty } from '@nestjs/swagger';
17
+
18
+ export class CreateItemDto {
19
+ @ApiProperty({ description: 'Item name', minLength: 1, maxLength: 100 })
20
+ @IsString()
21
+ @IsNotEmpty()
22
+ @MaxLength(100)
23
+ name: string;
24
+ }
25
+ ```
26
+ 3. **Create response DTO:**
27
+ ```typescript
28
+ export class ItemResponseDto {
29
+ @ApiProperty()
30
+ id: string;
31
+
32
+ @ApiProperty()
33
+ name: string;
34
+
35
+ @ApiProperty()
36
+ createdAt: Date;
37
+ }
38
+ ```
39
+ 4. **Add controller method** — THIN pattern:
40
+ ```typescript
41
+ @Post()
42
+ @ApiOperation({ summary: 'Create a new item' })
43
+ @ApiResponse({ status: 201, type: ItemResponseDto })
44
+ @ApiResponse({ status: 400, type: ErrorResponseDto })
45
+ async create(@Body() dto: CreateItemDto): Promise<ItemResponseDto> {
46
+ return this.commandBus.execute(new CreateItemCommand(dto.name, dto.description));
47
+ }
48
+ ```
49
+ 5. Register handler in module's `providers` array (if not done in step 1)
50
+ 6. **Add backend tests** — run `/write-backend-tests` for handler, validator, and controller
51
+ 7. `npm run build && npm run test`
52
+
53
+ ## Rules
54
+ - Controller does NOTHING except parse request → commandBus/queryBus → return
55
+ - DTO uses class-validator for shape validation; business rules stay in the Zod validator (created by `/add-handler`)
56
+ - ALL repo access via `getTransactionalRepo()` — UnitOfWork is automatic
57
+ - Every endpoint annotated with `@ApiOperation`, `@ApiResponse`, `@ApiBody`, `@ApiTags`
58
+ - All API responses use typed response DTOs — never raw entity objects
59
+ - Error responses use RFC 9457 problem-details shape
@@ -0,0 +1,68 @@
1
+ # Add Authenticated Endpoint
2
+
3
+ ## Auth Flow (BFF Pattern)
4
+ ```
5
+ Browser → httpOnly cookie → NestJS BFF middleware → injects Bearer header → JwtAuthGuard → RolesGuard → Handler
6
+ ```
7
+
8
+ The frontend sends **no** Authorization header. The BFF middleware extracts the JWT from the httpOnly session cookie and injects it as a Bearer token before the request reaches the controller.
9
+
10
+ ## Steps
11
+ 1. **Add controller endpoint** with appropriate decorators:
12
+ ```typescript
13
+ @Controller('items')
14
+ export class ItemsController {
15
+ constructor(private readonly commandBus: CommandBus) {}
16
+
17
+ // Protected endpoint (default — JwtAuthGuard is global)
18
+ @Post()
19
+ async create(@Body() dto: CreateItemDto, @Req() req: any) {
20
+ return this.commandBus.execute(new CreateItemCommand(dto.name, req.user.keycloakId));
21
+ }
22
+
23
+ // Admin-only endpoint
24
+ @Roles('admin')
25
+ @Get('admin/stats')
26
+ async stats() { ... }
27
+
28
+ // Public endpoint (no auth required)
29
+ @Public()
30
+ @Get('health')
31
+ async health() { ... }
32
+ }
33
+ ```
34
+ 2. **Extract user from request** — `req.user` contains:
35
+ ```typescript
36
+ {
37
+ keycloakId: string; // JWT sub
38
+ email: string;
39
+ roles: string[]; // from realm_access.roles
40
+ username?: string; // from preferred_username
41
+ }
42
+ ```
43
+ Note: `organizationId` is NOT in the JWT (multi-tenancy/RLS deferred per ADR-005).
44
+
45
+ ## Decorators
46
+ | Decorator | Effect |
47
+ |-----------|--------|
48
+ | *(none)* | JwtAuthGuard is global — endpoint is protected by default |
49
+ | `@Public()` | Bypass JwtAuthGuard — endpoint is public |
50
+ | `@Roles('admin')` | Require specific realm role(s) |
51
+ | `@Roles('admin', 'super-admin')` | Require ANY of the listed roles |
52
+
53
+ ## BFF Auth Endpoints (in `src/bff/`)
54
+ | Endpoint | Purpose |
55
+ |----------|---------|
56
+ | `GET /auth/login` | Redirect to Keycloak (params: `provider`, `returnTo`) |
57
+ | `GET /auth/callback` | OIDC code exchange → set httpOnly cookie → redirect to SPA |
58
+ | `POST /auth/refresh` | Refresh access token server-side |
59
+ | `POST /auth/logout` | Clear cookie + revoke Keycloak session |
60
+ | `GET /auth/me` | Return current user info from session |
61
+
62
+ ## Rules
63
+ - NEVER bypass auth for endpoints that mutate data
64
+ - NEVER return raw tokens to the frontend — only user profile data via `/auth/me`
65
+ - NEVER store tokens in `localStorage`, `sessionStorage`, or non-httpOnly cookies
66
+ - Frontend sends requests with `credentials: 'include'` — cookies are sent automatically
67
+ - NEVER set `Authorization` headers from frontend code
68
+ - NEVER implement token refresh logic in the frontend — BFF middleware handles it
@@ -0,0 +1,56 @@
1
+ # Add Entity
2
+
3
+ ## Steps
4
+ 1. **Create entity file** in `src/<module>/entities/<EntityName>.entity.ts`
5
+ 2. **Choose base class:**
6
+ - `BaseEntity` — standard base class. Provides `id` (UUID), `createdAt`, `updatedAt`.
7
+ - `TenantBaseEntity` — **only when multi-tenancy is enabled** (currently deferred per ADR-005). Adds `organizationId` column + RLS. Do NOT use until multi-tenancy is re-activated.
8
+ ```typescript
9
+ import { BaseEntity } from '@quanticjs/core';
10
+ ```
11
+ 3. **Define columns:**
12
+ ```typescript
13
+ import { BaseEntity } from '@quanticjs/core';
14
+
15
+ @Entity('items')
16
+ export class Item extends BaseEntity {
17
+ @Column({ type: 'varchar', length: 200 })
18
+ name!: string;
19
+
20
+ @Column({ type: 'text', nullable: true })
21
+ description!: string | null;
22
+
23
+ @Column({ type: 'jsonb', default: {} })
24
+ metadata!: Record<string, unknown>;
25
+
26
+ @Column({ type: 'boolean', default: true })
27
+ isActive!: boolean;
28
+ }
29
+ ```
30
+ 4. **Register in module** — add to `TypeOrmModule.forFeature([...])` in the module
31
+ 5. **Generate migration:** `npx typeorm migration:generate src/migrations/<Name>`
32
+ 6. **Run migration:** `npx typeorm migration:run`
33
+
34
+ ## Column Type Reference
35
+ | TypeScript | PostgreSQL | Notes |
36
+ |------------|------------|-------|
37
+ | `string` | `varchar(N)` | Always set length |
38
+ | `string` | `text` | Unlimited length |
39
+ | `number` | `int` | Integer |
40
+ | `number` | `decimal(10,2)` | Money/precision |
41
+ | `boolean` | `boolean` | |
42
+ | `Date` | `timestamptz` | Always use timestamptz |
43
+ | `Record<string,unknown>` | `jsonb` | Queryable JSON |
44
+ | `string[]` | `varchar[]` | PostgreSQL array |
45
+ | `enum` | `enum` | TypeORM enum type |
46
+
47
+ ## Rules
48
+ - Use `BaseEntity` for all entities (multi-tenancy/RLS is deferred per ADR-005)
49
+ - Do NOT use `TenantBaseEntity` or add `organizationId` until multi-tenancy is re-activated
50
+ - `BaseEntity` provides: `id` (UUID v4), `createdAt`, `updatedAt` — never redefine these
51
+ - Use `!` (definite assignment) on all columns — TypeORM sets them
52
+ - Database columns are **camelCase** (TypeORM default naming strategy)
53
+ - NEVER modify a migration that has been run — create a new one
54
+ - Index columns used in WHERE clauses: `@Index(['columnName'])`
55
+ - Import base classes from `@quanticjs/core`
56
+ - Each module owns its own PostgreSQL schema
@@ -0,0 +1,127 @@
1
+ # Add Domain Event
2
+
3
+ ## Event Flow
4
+ ```
5
+ Handler → OutboxEvent (same DB transaction) → OutboxPublisherService (poll) → Redis Stream → Consumer
6
+ ```
7
+
8
+ Events use the **outbox pattern** — the event record is written to the database in the same transaction as the entity mutation, guaranteeing atomicity. A background publisher polls the outbox and pushes to Redis Streams.
9
+
10
+ ## Steps
11
+ 1. **Create domain event in the handler** after successful mutation:
12
+ ```typescript
13
+ import { getTransactionalRepo, Result } from '@quanticjs/core';
14
+ import { OutboxEvent, DomainEvent } from '@quanticjs/core';
15
+
16
+ @CommandHandler(CreateItemCommand)
17
+ export class CreateItemHandler implements ICommandHandler<CreateItemCommand> {
18
+ constructor(
19
+ @InjectRepository(Item) private readonly itemRepo: Repository<Item>,
20
+ @InjectRepository(OutboxEvent) private readonly outboxRepo: Repository<OutboxEvent>,
21
+ ) {}
22
+
23
+ async execute(command: CreateItemCommand): Promise<Result<ItemDto>> {
24
+ const itemRepo = getTransactionalRepo(this.itemRepo);
25
+ const outboxRepo = getTransactionalRepo(this.outboxRepo);
26
+
27
+ const item = itemRepo.create({ name: command.name });
28
+ await itemRepo.save(item);
29
+
30
+ // Domain event — same transaction as the entity write
31
+ const event = new DomainEvent(
32
+ 'item.created', // eventType
33
+ item.id, // aggregateId
34
+ { name: item.name }, // payload (minimal — IDs preferred)
35
+ );
36
+
37
+ await outboxRepo.save(outboxRepo.create({
38
+ eventType: event.eventType,
39
+ aggregateId: event.aggregateId,
40
+ streamKey: event.streamKey,
41
+ payload: event.payload,
42
+ }));
43
+
44
+ return Result.success(toDto(item));
45
+ }
46
+ }
47
+ ```
48
+
49
+ 2. **Stream key** is derived from eventType: `item.created` → `autoflux:events:items`
50
+
51
+ 3. **OutboxPublisherService** (from `@quanticjs/core`) polls pending events:
52
+ - Reads pending OutboxEvents
53
+ - Publishes to Redis Stream
54
+ - Marks as Published or Failed (max 5 retries with exponential backoff → DLQ)
55
+
56
+ ## Event Naming Convention
57
+ | Event Type | Stream Key | When |
58
+ |------------|------------|------|
59
+ | `item.created` | `autoflux:events:items` | After entity creation |
60
+ | `item.updated` | `autoflux:events:items` | After entity update |
61
+ | `item.deleted` | `autoflux:events:items` | After soft delete |
62
+ | `project.status.changed` | `autoflux:events:projects` | After status transition |
63
+
64
+ ## Consuming Events
65
+
66
+ Extend `RedisStreamConsumer` from `@quanticjs/core`. The base class automatically creates a **dedicated Redis connection** for blocking `XREADGROUP BLOCK` calls, keeping the shared `REDIS_CLIENT` free for non-blocking ops.
67
+
68
+ ```typescript
69
+ import { Injectable, Inject, Optional } from '@nestjs/common';
70
+ import { REDIS_CLIENT, RedisStreamConsumer } from '@quanticjs/core';
71
+ import type { Redis } from 'ioredis';
72
+ import { hostname } from 'os';
73
+
74
+ @Injectable()
75
+ export class ItemEventConsumer extends RedisStreamConsumer {
76
+ readonly streamKey = 'autoflux:events:items';
77
+ readonly consumerGroup = 'project-planning';
78
+ readonly consumerName = `planner-${hostname()}-${process.pid}`;
79
+
80
+ constructor(@Optional() @Inject(REDIS_CLIENT) redis: Redis | undefined) {
81
+ super(redis);
82
+ }
83
+
84
+ protected shouldHandle(fields: Record<string, string>): boolean {
85
+ return fields.eventType === 'item.created';
86
+ }
87
+
88
+ async handleMessage(fields: Record<string, string>): Promise<void> {
89
+ const payload = JSON.parse(fields.payload || '{}');
90
+ // Handle idempotently — events may be delivered more than once
91
+ }
92
+ }
93
+ ```
94
+
95
+ ## Lifecycle Hooks
96
+ ```typescript
97
+ // Setup in onModuleInit, polling in onApplicationBootstrap
98
+ async onModuleInit() {
99
+ await this.createConsumerGroup(); // XGROUP CREATE
100
+ }
101
+
102
+ async onApplicationBootstrap() {
103
+ this.startPolling(); // non-blocking — fires and forgets
104
+ }
105
+ ```
106
+
107
+ ## Connection Layout
108
+ | Client | Count | Purpose |
109
+ |--------|-------|---------|
110
+ | `REDIS_CLIENT` (shared) | 1 | Cache, locks, XADD, XGROUP CREATE, XACK |
111
+ | Dedicated blocking client | 1 per consumer | `XREADGROUP BLOCK` only |
112
+
113
+ ## Retry & Dead-Letter Policy
114
+ - Max retries: 5 with exponential backoff + jitter
115
+ - Backoff: `min(1s × 2^attempt + random(0,1000ms), 30s)`
116
+ - Dead-letter stream: `{stream}:dlq` with `MAXLEN ~ 100000`
117
+ - Failed events are NEVER silently dropped
118
+
119
+ ## Rules
120
+ - ALWAYS use outbox pattern — never publish directly to Redis (data loss on crash)
121
+ - OutboxEvent is saved in the SAME transaction as the entity mutation
122
+ - Event payloads should be minimal — include IDs, not full entities
123
+ - Consumers must be idempotent — events may be delivered more than once
124
+ - NEVER share Redis connection for blocking XREADGROUP reads
125
+ - NEVER start polling in `onModuleInit()` — use `onApplicationBootstrap()`
126
+ - NEVER publish events before the transaction commits
127
+ - All repo access via `getTransactionalRepo()`
@@ -0,0 +1,20 @@
1
+ # Add Feature
2
+
3
+ End-to-end feature implementation orchestrating all sub-skills.
4
+
5
+ ## Usage
6
+ ```
7
+ /add-feature Create project items with priority and status
8
+ /add-feature User profile editing with avatar upload
9
+ ```
10
+
11
+ ## Steps
12
+ 1. **Understand requirements** — read any specs, clarify with user if ambiguous
13
+ 2. **Create command/query + validator + handler** — run `/add-handler`
14
+ 3. **Create/update entity** — run `/add-entity`
15
+ 4. **Create migration** — run `/add-migration`
16
+ 5. **Add controller endpoint** — run `/add-api-endpoint` (handler already exists from step 2, so only DTO + controller)
17
+ 6. **Add frontend** — run `/add-frontend-page` (skip if purely backend)
18
+ 7. **Add backend tests** — run `/write-backend-tests`
19
+ 8. **Add UI tests** — run `/write-ui-tests` (skip if no frontend)
20
+ 9. **Verify** — `npm run build && npm test`
@@ -0,0 +1,75 @@
1
+ # Add Frontend Page
2
+
3
+ ## Usage
4
+ ```
5
+ /add-frontend-page /projects
6
+ /add-frontend-page /settings/profile
7
+ ```
8
+
9
+ ## Steps
10
+ 1. Create route in React Router config
11
+ 2. Create page component in `client/src/pages/` — lazy-loaded with `React.lazy()` + `<Suspense>`
12
+ 3. Create data hooks using `@quanticjs/react-query`:
13
+ - `useApiQuery` for data fetching (NOT raw `useQuery`)
14
+ - `useApiMutation` for write operations with `invalidates` option (NOT raw `useMutation`)
15
+ - `usePaginatedQuery` for paginated lists
16
+ 4. Use shadcn/ui + `@quanticjs/react-ui` components (Spinner, Skeleton, EmptyState, ErrorBoundary, Dialog, ToastProvider)
17
+ 5. Add loading skeleton, empty state, and error boundary
18
+ 6. Forms use `useForm` from `@quanticjs/react-forms` + Zod (NOT raw React Hook Form) — auto-maps server validation errors
19
+ 7. Support dark mode via CSS variables: `hsl(var(--background))`, `hsl(var(--primary))`
20
+ 8. **Add tests** — run `/write-ui-tests` for the new page
21
+
22
+ ## Design Standards
23
+ - All async content has loading skeletons (`Skeleton` from `@quanticjs/react-ui`)
24
+ - All lists have empty states (`EmptyState` from `@quanticjs/react-ui`)
25
+ - All forms use `useForm` from `@quanticjs/react-forms` + Zod (auto server-error mapping)
26
+ - WCAG 2.1 AA accessible
27
+ - No hardcoded hex colors or spacing — use design tokens/CSS variables
28
+ - Components accept `className` for extension and forward refs
29
+
30
+ ## State Management (per ADR-003)
31
+ | State type | Tool |
32
+ |---|---|
33
+ | Server/remote data | `useApiQuery` / `useApiMutation` from `@quanticjs/react-query` |
34
+ | URL-derived state | `useSearchParams` |
35
+ | Local UI state | `useState` |
36
+ | Shared client state | Zustand store (with selectors) |
37
+ | Form state | `useForm` from `@quanticjs/react-forms` + Zod |
38
+
39
+ ## Error Handling — Three Tiers
40
+
41
+ | Tier | Handles | Pattern |
42
+ |---|---|---|
43
+ | **Forms** | Validation errors (400/422) | `useForm` auto-maps `ApiError.fieldErrors` to form fields. Non-field errors go to `errors._root`. |
44
+ | **Toast** | Non-form mutations (delete, toggle, actions) | `toast.error(apiError)` — ApiError-aware, extracts title + detail automatically |
45
+ | **ErrorBoundary** | Unhandled render errors | Already wired in app root provider stack. Prefer page-level boundary for granular recovery. |
46
+
47
+ Non-form mutations **must** always provide `onError`:
48
+ ```typescript
49
+ const mutation = useApiMutation(
50
+ (api, id: string) => api.delete(`/items/${id}`),
51
+ {
52
+ invalidates: [['items']],
53
+ onError: (error) => toast.error(error),
54
+ },
55
+ );
56
+ ```
57
+
58
+ **5xx errors:** Never show `error.detail` — may contain stack traces. Show generic "Something went wrong" message.
59
+ Always include `error.correlationId` in error UI for support reporting.
60
+
61
+ ## Rules
62
+ - NEVER use raw `useQuery`/`useMutation` — use `useApiQuery`/`useApiMutation` from `@quanticjs/react-query`
63
+ - NEVER use raw React Hook Form — use `useForm` from `@quanticjs/react-forms` (auto server-error mapping)
64
+ - NEVER fetch data with `useEffect` + `useState` — use `useApiQuery`
65
+ - NEVER copy query data into `useState`
66
+ - NEVER put server data in Zustand
67
+ - NEVER manually map server validation errors to form fields — `@quanticjs/react-forms` handles this
68
+ - NEVER omit `onError` on non-form mutations — every mutation failure must be visible to the user
69
+ - NEVER show `error.detail` from 5xx responses — use generic message instead
70
+ - NEVER write `catch (e) { console.log(e) }` on mutations — swallowing errors is a bug
71
+ - NEVER use `any` — use `unknown` and narrow
72
+ - NEVER hardcode hex colors or spacing values
73
+ - NEVER use `NodeJS.Timeout` or other Node.js types in frontend code
74
+ - API errors handled via `ApiError` from `@quanticjs/react-core` — use `.detail`, `.fieldErrors`, `.correlationId`
75
+ - Auth refresh handled automatically by `createClient`'s `auth.refresh` — NEVER implement retry logic on 401
@@ -0,0 +1,105 @@
1
+ # Add Handler
2
+
3
+ Create a CQRS command or query with validator and handler following the `@quanticjs/core` patterns.
4
+
5
+ ## Usage
6
+ ```
7
+ /add-handler CreateItem in project
8
+ /add-handler GetUserProfile in identity
9
+ ```
10
+
11
+ ## Steps
12
+
13
+ 1. **Create command or query class** in `src/<module>/commands/` or `src/<module>/queries/`
14
+ - Add `@Validate(XxxValidator)` decorator on the command class (MANDATORY)
15
+ - Add optional decorators as needed: `@Cache`, `@DistributedLock`, `@FeatureFlag`
16
+ ```typescript
17
+ import { Validate, DistributedLock } from '@quanticjs/core';
18
+
19
+ @Validate(CreateXxxValidator)
20
+ @DistributedLock('create-xxx:{name}') // only if critical section needed
21
+ export class CreateXxxCommand {
22
+ constructor(public readonly name: string, public readonly userId: string) {}
23
+ }
24
+ ```
25
+
26
+ 2. **Create `.validator.ts`** co-located with the command — ALL validation logic lives here:
27
+ ```typescript
28
+ import { z } from 'zod';
29
+ import { ICommandValidator, validateCommand } from '@quanticjs/core';
30
+
31
+ export class CreateXxxValidator implements ICommandValidator<CreateXxxCommand> {
32
+ private schema = z.object({
33
+ name: z.string().min(1).max(100),
34
+ // Business rules go here as .refine() / .superRefine()
35
+ });
36
+ validate(command: CreateXxxCommand) { return validateCommand(this.schema, command); }
37
+ }
38
+ ```
39
+
40
+ 3. **Create handler class** implementing `ICommandHandler<T>` or `IQueryHandler<T>`:
41
+ ```typescript
42
+ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
43
+ import { InjectRepository } from '@nestjs/typeorm';
44
+ import { Repository } from 'typeorm';
45
+ import { getTransactionalRepo, Result } from '@quanticjs/core';
46
+
47
+ @CommandHandler(CreateXxxCommand)
48
+ export class CreateXxxHandler implements ICommandHandler<CreateXxxCommand> {
49
+ constructor(@InjectRepository(Xxx) private readonly xxxRepo: Repository<Xxx>) {}
50
+
51
+ async execute(command: CreateXxxCommand): Promise<Result<XxxDto>> {
52
+ const xxxRepo = getTransactionalRepo(this.xxxRepo); // UnitOfWork
53
+ const entity = xxxRepo.create({ name: command.name });
54
+ await xxxRepo.save(entity);
55
+ return Result.success(toDto(entity));
56
+ }
57
+ }
58
+ ```
59
+
60
+ 4. **Register** handler and validator in module's `providers` array
61
+ 5. **Add tests** — run `/write-backend-tests` for the handler and validator
62
+
63
+ ## Pipeline Behavior Chain
64
+ **Commands:** `Log (global) → FeatureFlag → Validate → Cache → DistributedLock → Transactional (auto) → Handler`
65
+ **Queries:** `Log (global) → FeatureFlag → Validate → Cache → Handler`
66
+
67
+ ## Available Decorators
68
+ | Decorator | When to Use |
69
+ |-----------|-------------|
70
+ | `@Validate(ValidatorClass)` | Every command with external input (MANDATORY) |
71
+ | `@DistributedLock('key:{prop}')` | Commands with critical sections — race conditions, concurrent writes, resource contention |
72
+ | `@Cache('key:{prop}', { ttlSeconds })` | Read-heavy queries |
73
+ | `@FeatureFlag('release-module-feature')` | Feature-gated commands (see naming below) |
74
+ | `@IsolatedTransaction()` | Audit/notification commands that must commit independently |
75
+
76
+ ## Feature Flag Naming & Lifecycle
77
+
78
+ When adding `@FeatureFlag`, use the correct naming convention and fallback:
79
+
80
+ | Category | Name format | Lifetime | Fallback |
81
+ |---|---|---|---|
82
+ | **Release** | `release-{module}-{feature}` | Remove within 30 days of full rollout | `throw` (default) |
83
+ | **Kill switch** | `kill-{module}-{feature}` | Permanent | `throw` (default) |
84
+ | **Experiment** | `experiment-{module}-{feature}` | Remove within 90 days | `default` (control variant) |
85
+
86
+ ```typescript
87
+ @FeatureFlag('release-billing-invoices') // blocks if disabled
88
+ @FeatureFlag('kill-payments-processing') // blocks if disabled
89
+ @FeatureFlag('experiment-scoring-v2', { fallback: 'default', defaultValue: oldResult })
90
+ @FeatureFlag('release-notifications-email', { fallback: 'skip' }) // silently skips
91
+ ```
92
+
93
+ If `UNLEASH_URL` is not set, all flags pass — local dev and tests work without Unleash.
94
+
95
+ ## Rules
96
+ - Command class MUST have `@Validate(XxxValidator)` — without it, the `.validator.ts` is dead code
97
+ - ALL validation in `.validator.ts` using Zod — NEVER validate inline in handlers
98
+ - Business rules (age >= 18, email unique, date range valid) → Zod `.refine()` / `.superRefine()` in validator
99
+ - Handler uses `getTransactionalRepo()` for all repo access — UnitOfWork is automatic
100
+ - Handlers NEVER contain validation logic — no `if (x < y) return Result.validationError()`
101
+ - Handlers NEVER throw exceptions — return `Result.failure()` / `Result.notFound()` / `Result.conflict()` etc.
102
+ - If handler has a critical section, add `@DistributedLock('key:{prop}')` on the **command class**
103
+ - Return `Result<T>` from handlers — never throw for business errors
104
+ - Feature flags: NEVER nest multiple `@FeatureFlag` on one handler — one handler, one flag
105
+ - Feature flags: NEVER use on infrastructure code (migrations, middleware) — use env vars instead