@sulhadin/orchestrator 2.0.0 → 3.0.0-beta
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.
- package/README.md +49 -74
- package/bin/index.js +136 -82
- package/package.json +1 -1
- package/template/.claude/agents/conductor.md +146 -0
- package/template/.claude/agents/reviewer.md +88 -0
- package/template/.claude/commands/orchestra/blueprint.md +23 -0
- package/template/.claude/commands/orchestra/help.md +49 -0
- package/template/.claude/commands/orchestra/hotfix.md +13 -0
- package/template/.claude/commands/orchestra/pm.md +7 -0
- package/template/.claude/commands/orchestra/start.md +13 -0
- package/template/.claude/commands/orchestra/status.md +11 -0
- package/template/.claude/conductor.md +146 -0
- package/template/.claude/rules/acceptance-check.orchestra.md +13 -0
- package/template/.claude/rules/code-standards.orchestra.md +15 -0
- package/template/.claude/rules/commit-format.orchestra.md +14 -0
- package/template/.claude/rules/phase-limits.orchestra.md +21 -0
- package/template/.claude/rules/stuck-detection.orchestra.md +25 -0
- package/template/.claude/rules/testing-standards.orchestra.md +10 -0
- package/template/.claude/rules/verification-gate.orchestra.md +24 -0
- package/template/.claude/skills/fullstack-infrastructure.orchestra.md +810 -0
- package/template/.orchestra/README.md +10 -14
- package/template/.orchestra/config.yml +36 -0
- package/template/.orchestra/knowledge.md +4 -23
- package/template/.orchestra/roles/adaptive.md +14 -87
- package/template/.orchestra/roles/architect.md +17 -407
- package/template/.orchestra/roles/backend-engineer.md +13 -357
- package/template/.orchestra/roles/frontend-engineer.md +14 -419
- package/template/.orchestra/roles/orchestrator.md +48 -0
- package/template/.orchestra/roles/product-manager.md +73 -590
- package/template/CLAUDE.md +39 -139
- package/template/.orchestra/agents/worker.md +0 -557
- package/template/.orchestra/roles/code-reviewer.md +0 -265
- package/template/.orchestra/roles/owner.md +0 -290
- /package/template/{.orchestra/skills/accessibility.md → .claude/skills/accessibility.orchestra.md} +0 -0
- /package/template/{.orchestra/skills/auth-setup.md → .claude/skills/auth-setup.orchestra.md} +0 -0
- /package/template/{.orchestra/skills/best-practices.md → .claude/skills/best-practices.orchestra.md} +0 -0
- /package/template/{.orchestra/skills/code-optimizer.md → .claude/skills/code-optimizer.orchestra.md} +0 -0
- /package/template/{.orchestra/skills/core-web-vitals.md → .claude/skills/core-web-vitals.orchestra.md} +0 -0
- /package/template/{.orchestra/skills/crud-api.md → .claude/skills/crud-api.orchestra.md} +0 -0
- /package/template/{.orchestra/skills/debug.md → .claude/skills/debug.orchestra.md} +0 -0
- /package/template/{.orchestra/skills/deployment.md → .claude/skills/deployment.orchestra.md} +0 -0
- /package/template/{.orchestra/skills/frontend-design.md → .claude/skills/frontend-design.orchestra.md} +0 -0
- /package/template/{.orchestra/skills/react-best-practices.md → .claude/skills/react-best-practices.orchestra.md} +0 -0
- /package/template/{.orchestra/skills/review.md → .claude/skills/review.orchestra.md} +0 -0
- /package/template/{.orchestra/skills/testing.md → .claude/skills/testing.orchestra.md} +0 -0
- /package/template/{.orchestra/skills/web-quality-audit.md → .claude/skills/web-quality-audit.orchestra.md} +0 -0
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fullstack-infra
|
|
3
|
+
description: |
|
|
4
|
+
Fullstack TypeScript project infrastructure and coding standards skill. Use this skill whenever the user starts a new project, creates components, writes backend services, sets up a monorepo, scaffolds features, or writes any code in an existing TypeScript project. Also trigger when the user asks about project structure, coding conventions, component organization, API design, state management, or best practices. This skill defines mandatory standards — every piece of code must comply.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Fullstack TypeScript Infrastructure & Standards
|
|
8
|
+
|
|
9
|
+
This skill defines the architecture, conventions, and coding standards for all TypeScript projects. Every piece of code you write or modify must follow these rules. These are not suggestions — they are the engineering standards of the team.
|
|
10
|
+
|
|
11
|
+
Before writing any code, ask these project-scoping questions if not already answered:
|
|
12
|
+
- Is this a monorepo or single repo?
|
|
13
|
+
- Is the backend Express or Hono?
|
|
14
|
+
- Is i18n (multi-language support) needed?
|
|
15
|
+
- REST or GraphQL? (default: REST)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Tech Stack
|
|
20
|
+
|
|
21
|
+
| Layer | Technology |
|
|
22
|
+
|------------------|-----------------------------------|
|
|
23
|
+
| Language | TypeScript (strict mode, always) |
|
|
24
|
+
| Frontend Build | Vite |
|
|
25
|
+
| Mobile | Expo (React Native) |
|
|
26
|
+
| State Management | Zustand (web & mobile) |
|
|
27
|
+
| Styling | Tailwind CSS (primary) + SCSS (edge cases only) |
|
|
28
|
+
| Web UI Components| shadcn/ui |
|
|
29
|
+
| HTTP Client | Axios (via wrapper) |
|
|
30
|
+
| Data Fetching | SWR (caches backend responses, uses Axios wrapper internally) |
|
|
31
|
+
| Backend | Express or Hono (ask which one) |
|
|
32
|
+
| ORM | Prisma |
|
|
33
|
+
| Auth | JWT |
|
|
34
|
+
| Testing | Vitest (unit), Playwright (E2E) |
|
|
35
|
+
| Linter/Formatter | Biome |
|
|
36
|
+
| Monorepo | Yarn Workspaces |
|
|
37
|
+
| Git | Trunk-based development, Conventional Commits |
|
|
38
|
+
| Env Validation | Zod schemas for .env variables |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Core Principles
|
|
43
|
+
|
|
44
|
+
These principles are non-negotiable. Every function, component, and module must satisfy them.
|
|
45
|
+
|
|
46
|
+
### SOLID
|
|
47
|
+
|
|
48
|
+
- **Single Responsibility**: One reason to change per module/class/function. A component renders UI — it does not fetch data, transform it, and handle errors all in one place.
|
|
49
|
+
- **Open/Closed**: Extend behavior through composition and props, not by modifying existing code. Use strategy patterns, render props, or hooks to add behavior.
|
|
50
|
+
- **Liskov Substitution**: Subtypes must be substitutable. If a component accepts `ButtonProps`, any extension of `ButtonProps` must work without breaking.
|
|
51
|
+
- **Interface Segregation**: Keep interfaces small and focused. Don't force consumers to depend on methods or props they don't use. Split large interfaces into smaller, role-specific ones.
|
|
52
|
+
- **Dependency Inversion**: Depend on abstractions. Services should accept interfaces, not concrete implementations. This enables testing and swapping implementations.
|
|
53
|
+
|
|
54
|
+
### KISS
|
|
55
|
+
|
|
56
|
+
Write the simplest code that solves the problem. If a solution needs a comment to explain what it does, it's probably too complex. Prefer readable, obvious code over clever code.
|
|
57
|
+
|
|
58
|
+
### YAGNI
|
|
59
|
+
|
|
60
|
+
Don't build for hypothetical future requirements. No feature flags for features that don't exist. No abstractions for a single use case. Three similar lines of code are better than a premature abstraction. Build what's needed now.
|
|
61
|
+
|
|
62
|
+
### DRY (with judgment)
|
|
63
|
+
|
|
64
|
+
Avoid duplication, but don't abstract prematurely. If code is duplicated in 2+ places and serves the same purpose, extract it. If two pieces of code look similar but serve different domains, they can stay separate — they'll likely diverge over time.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Workarounds Are Forbidden
|
|
69
|
+
|
|
70
|
+
Never apply a workaround, hack, or quick fix without explicit user approval. If you encounter a situation where the "right" solution seems too complex or time-consuming:
|
|
71
|
+
|
|
72
|
+
1. Stop and explain the problem clearly
|
|
73
|
+
2. Present the proper solution and the workaround
|
|
74
|
+
3. Wait for explicit approval before applying a workaround
|
|
75
|
+
|
|
76
|
+
If the user approves, add a `// WORKAROUND:` comment explaining why it exists and link to a tracking issue if possible.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Monorepo Structure
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
monorepo/
|
|
84
|
+
├── apps/
|
|
85
|
+
│ ├── <project>/ # Web app (Vite, Playwright E2E)
|
|
86
|
+
│ │ └── src/
|
|
87
|
+
│ │ ├── app/ # App config, routing, providers
|
|
88
|
+
│ │ ├── assets/ # Static files (images, fonts, icons)
|
|
89
|
+
│ │ ├── components/ # Shared components (used by 2+ modules)
|
|
90
|
+
│ │ ├── libs/ # App-specific shared utilities (used by 2+ modules within this app)
|
|
91
|
+
│ │ ├── modules/ # Feature modules (pages, widgets)
|
|
92
|
+
│ │ │ └── <domain>/
|
|
93
|
+
│ │ │ ├── components/ # Domain-specific components
|
|
94
|
+
│ │ │ ├── utils/ # Domain-specific utilities
|
|
95
|
+
│ │ │ └── <Page>.tsx
|
|
96
|
+
│ │ ├── store/ # Zustand stores
|
|
97
|
+
│ │ └── types/ # TypeScript type definitions
|
|
98
|
+
│ │
|
|
99
|
+
│ ├── <mobile>/ # Mobile app (Expo)
|
|
100
|
+
│ │ └── src/
|
|
101
|
+
│ │ ├── app/ # Expo Router / navigation
|
|
102
|
+
│ │ ├── components/ # Shared mobile components (2+ screens)
|
|
103
|
+
│ │ ├── design/ # Mobile UI components (isolated, minimal deps)
|
|
104
|
+
│ │ ├── libs/ # App-specific shared utilities
|
|
105
|
+
│ │ ├── modules/ # Feature modules
|
|
106
|
+
│ │ │ └── <domain>/
|
|
107
|
+
│ │ │ ├── components/
|
|
108
|
+
│ │ │ └── utils/
|
|
109
|
+
│ │ ├── store/ # Zustand stores
|
|
110
|
+
│ │ └── types/
|
|
111
|
+
│ │
|
|
112
|
+
│ └── api/ # Backend (Express or Hono)
|
|
113
|
+
│ └── src/
|
|
114
|
+
│ ├── routes/ # Route definitions
|
|
115
|
+
│ ├── controllers/ # Request/response handling (Express)
|
|
116
|
+
│ ├── handlers/ # Request handling (Hono — use instead of controllers/)
|
|
117
|
+
│ ├── services/ # Business logic (pure, testable)
|
|
118
|
+
│ ├── middlewares/ # Auth, error handler, validation, logging
|
|
119
|
+
│ ├── models/ # Prisma models / type definitions
|
|
120
|
+
│ ├── utils/ # Helper functions
|
|
121
|
+
│ ├── config/ # Env validation (Zod), DB config, constants
|
|
122
|
+
│ └── index.ts # App entry point
|
|
123
|
+
│
|
|
124
|
+
├── packages/
|
|
125
|
+
│ ├── design/ # Design system (Atomic Design)
|
|
126
|
+
│ │ └── src/
|
|
127
|
+
│ │ ├── assets/ # Design tokens, icons, fonts
|
|
128
|
+
│ │ ├── components/
|
|
129
|
+
│ │ │ ├── atoms/ # Button, Input, Icon, Text, Badge
|
|
130
|
+
│ │ │ ├── elements/ # FormField, Card, Tooltip, Avatar
|
|
131
|
+
│ │ │ ├── molecules/ # SearchBar, NavItem, Dropdown, Modal
|
|
132
|
+
│ │ │ └── organisms/ # Header, Sidebar, DataTable, Form
|
|
133
|
+
│ │ ├── helpers/ # Design-related utilities
|
|
134
|
+
│ │ ├── hoc/ # Higher-order components
|
|
135
|
+
│ │ └── hooks/ # UI-related hooks
|
|
136
|
+
│ │
|
|
137
|
+
│ └── utils/ # Shared utilities across all apps
|
|
138
|
+
│ └── src/
|
|
139
|
+
│ ├── apis/ # API endpoint definitions
|
|
140
|
+
│ ├── client/ # HTTP client (Axios wrapper)
|
|
141
|
+
│ ├── constants/ # Shared constants
|
|
142
|
+
│ ├── hooks/ # Shared hooks (including SWR hooks)
|
|
143
|
+
│ ├── network/ # Network utilities, interceptors
|
|
144
|
+
│ ├── performance/ # Performance monitoring utilities
|
|
145
|
+
│ ├── ws/ # WebSocket client class
|
|
146
|
+
│ └── utils/ # General utility functions
|
|
147
|
+
│
|
|
148
|
+
├── .github/ # CI/CD workflows
|
|
149
|
+
├── biome.json # Linter/formatter config
|
|
150
|
+
├── tsconfig.base.json # Shared TypeScript config
|
|
151
|
+
└── package.json # Root workspace config (Yarn)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
For **single-repo** projects, use the same structure but with only one app (no `packages/` unless shared code emerges naturally).
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Component Architecture
|
|
159
|
+
|
|
160
|
+
### Web Components (shadcn/ui approach)
|
|
161
|
+
|
|
162
|
+
Components are installed via shadcn/ui CLI and customized in-place. Each component lives in its own folder:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
components/
|
|
166
|
+
└── Button/
|
|
167
|
+
├── Button.tsx
|
|
168
|
+
├── Button.scss # Only if Tailwind alone can't handle it
|
|
169
|
+
└── index.ts # Re-export
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Rules:
|
|
173
|
+
- shadcn/ui is the foundation for all web UI components
|
|
174
|
+
- Tailwind CSS is the primary styling approach — use SCSS only for complex animations, pseudo-element tricks, or cases Tailwind genuinely can't cover
|
|
175
|
+
- If a third-party UI dependency is added, wrap it in a component inside `packages/design` so it can be swapped later without touching consumers
|
|
176
|
+
- Every component must be reusable by design — accept props for customization, avoid hardcoded values
|
|
177
|
+
|
|
178
|
+
### Mobile Components (minimal dependency approach)
|
|
179
|
+
|
|
180
|
+
Mobile uses the same folder structure but without shadcn/ui. Build components from scratch with minimal external dependencies:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
design/
|
|
184
|
+
└── Button/
|
|
185
|
+
├── Button.tsx
|
|
186
|
+
└── index.ts
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Rules:
|
|
190
|
+
- Minimize external dependencies — fewer deps means faster builds and fewer breaking changes
|
|
191
|
+
- Build custom components that match the design system
|
|
192
|
+
- Share logic (not UI) with web through `packages/utils`
|
|
193
|
+
|
|
194
|
+
### Component Placement Decision Tree
|
|
195
|
+
|
|
196
|
+
When creating a new component, follow this decision tree:
|
|
197
|
+
|
|
198
|
+
1. **Is it a pure UI primitive with no business logic?** (Button, Input, Modal)
|
|
199
|
+
→ `packages/design/src/components/<atomic-level>/`
|
|
200
|
+
|
|
201
|
+
2. **Is it used by 2+ modules within the same app?**
|
|
202
|
+
→ `apps/<app>/src/components/`
|
|
203
|
+
|
|
204
|
+
3. **Is it used by only one module?**
|
|
205
|
+
→ `apps/<app>/src/modules/<domain>/components/`
|
|
206
|
+
|
|
207
|
+
The same logic applies to utility functions:
|
|
208
|
+
|
|
209
|
+
1. **Is it used across multiple apps?**
|
|
210
|
+
→ `packages/utils/src/` (must have JSDoc with `@example`)
|
|
211
|
+
|
|
212
|
+
2. **Is it used by 2+ modules within one app?**
|
|
213
|
+
→ `apps/<app>/src/libs/` (must have JSDoc with `@example`)
|
|
214
|
+
|
|
215
|
+
3. **Is it used by only one module?**
|
|
216
|
+
→ `apps/<app>/src/modules/<domain>/utils/`
|
|
217
|
+
|
|
218
|
+
### Classes vs Utility Functions
|
|
219
|
+
|
|
220
|
+
Use classes when there's state, configuration, or a third-party integration to encapsulate. Use plain functions for stateless, single-purpose utilities.
|
|
221
|
+
|
|
222
|
+
**When to use a class:**
|
|
223
|
+
- **Third-party API integrations**: Every external API (Stripe, SendGrid, AWS S3, etc.) gets its own class that wraps the SDK. This isolates the dependency, makes it swappable, and provides a consistent internal interface:
|
|
224
|
+
```typescript
|
|
225
|
+
// services/PaymentService.ts
|
|
226
|
+
export class PaymentService {
|
|
227
|
+
private client: Stripe;
|
|
228
|
+
|
|
229
|
+
constructor(apiKey: string) {
|
|
230
|
+
this.client = new Stripe(apiKey);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async createCharge(amount: number, currency: string): Promise<Charge> {
|
|
234
|
+
return this.client.charges.create({ amount, currency });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async refund(chargeId: string): Promise<Refund> {
|
|
238
|
+
return this.client.refunds.create({ charge: chargeId });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
- WebSocket connections, database clients, or anything with lifecycle (init/destroy)
|
|
243
|
+
- When multiple related methods share configuration or state
|
|
244
|
+
|
|
245
|
+
**When to use a plain function:**
|
|
246
|
+
- Stateless, single-purpose transformations (`formatDate`, `slugify`, `calculateTax`)
|
|
247
|
+
- Pure utility logic with no side effects
|
|
248
|
+
|
|
249
|
+
**Dependency policy for utilities:**
|
|
250
|
+
- Prefer dependency-free implementations for small utilities — write them in `packages/utils/` with JSDoc + `@example`
|
|
251
|
+
- Use an npm package only when: the implementation is long and complex, it's out of scope for the project (e.g., date parsing, cryptography), or maintaining it in-house would be a burden
|
|
252
|
+
- If in doubt, start dependency-free. It's easier to add a dependency later than to remove one
|
|
253
|
+
|
|
254
|
+
### Before Creating Any Function or Component
|
|
255
|
+
|
|
256
|
+
Always check `packages/utils/` and `apps/<app>/src/libs/` first. If a similar function exists:
|
|
257
|
+
- Extend it if it's missing functionality (without breaking existing consumers)
|
|
258
|
+
- Use it as-is if it covers the need
|
|
259
|
+
- Never create a duplicate
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Atomic Design Levels
|
|
264
|
+
|
|
265
|
+
| Level | Description | Examples |
|
|
266
|
+
|-----------|--------------------------------------|-----------------------------------|
|
|
267
|
+
| Atoms | Smallest indivisible UI units | Button, Input, Icon, Text, Badge |
|
|
268
|
+
| Elements | Single-purpose composed atoms | FormField, Card, Tooltip, Avatar |
|
|
269
|
+
| Molecules | Functional groups with local state | SearchBar, NavItem, Dropdown |
|
|
270
|
+
| Organisms | Complex sections with business logic | Header, Sidebar, DataTable, Form |
|
|
271
|
+
|
|
272
|
+
Rules:
|
|
273
|
+
- Atoms have no dependencies on other components
|
|
274
|
+
- Each level can only import from the same level or below
|
|
275
|
+
- Organisms can contain business logic; atoms, elements, and molecules must be pure UI
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## State Management (Zustand)
|
|
280
|
+
|
|
281
|
+
### Store Structure
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// store/useAuthStore.ts
|
|
285
|
+
interface AuthState {
|
|
286
|
+
user: User | null;
|
|
287
|
+
token: string | null;
|
|
288
|
+
login: (credentials: LoginCredentials) => Promise<void>;
|
|
289
|
+
logout: () => void;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export const useAuthStore = create<AuthState>()((set) => ({
|
|
293
|
+
user: null,
|
|
294
|
+
token: null,
|
|
295
|
+
login: async (credentials) => {
|
|
296
|
+
const { data } = await authService.login(credentials);
|
|
297
|
+
set({ user: data.user, token: data.token });
|
|
298
|
+
},
|
|
299
|
+
logout: () => set({ user: null, token: null }),
|
|
300
|
+
}));
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Rules:
|
|
304
|
+
- One store per domain (auth, cart, ui, etc.)
|
|
305
|
+
- Keep stores flat — no deep nesting
|
|
306
|
+
- Business logic (API calls) can live in store actions, but complex logic should be extracted to services
|
|
307
|
+
- Use selectors for derived state to avoid unnecessary re-renders:
|
|
308
|
+
```typescript
|
|
309
|
+
const userName = useAuthStore((state) => state.user?.name);
|
|
310
|
+
```
|
|
311
|
+
- Never put server-cache data in Zustand — that's SWR's job. Zustand is for client state (UI state, auth, preferences)
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Network Layer
|
|
316
|
+
|
|
317
|
+
### Architecture
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
packages/utils/src/
|
|
321
|
+
├── client/
|
|
322
|
+
│ ├── HttpClient.ts # Base Axios wrapper class (shared config, interceptors)
|
|
323
|
+
│ ├── UserApi.ts # Domain API class (extends or uses HttpClient)
|
|
324
|
+
│ ├── OrderApi.ts # Domain API class
|
|
325
|
+
│ └── index.ts # Export all API class instances
|
|
326
|
+
├── network/
|
|
327
|
+
│ └── interceptors.ts # Request/response interceptors (auth token, error transform)
|
|
328
|
+
└── hooks/
|
|
329
|
+
└── use<Resource>.ts # SWR hooks per resource (consume API classes)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Base HTTP Client
|
|
333
|
+
|
|
334
|
+
A single Axios-based class that all domain API classes inherit from:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// client/HttpClient.ts
|
|
338
|
+
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|
339
|
+
|
|
340
|
+
export class HttpClient {
|
|
341
|
+
protected client: AxiosInstance;
|
|
342
|
+
|
|
343
|
+
constructor(baseURL: string, config?: AxiosRequestConfig) {
|
|
344
|
+
this.client = axios.create({
|
|
345
|
+
baseURL,
|
|
346
|
+
headers: { 'Content-Type': 'application/json' },
|
|
347
|
+
...config,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
this.client.interceptors.request.use((cfg) => {
|
|
351
|
+
const token = useAuthStore.getState().token;
|
|
352
|
+
if (token) cfg.headers.Authorization = `Bearer ${token}`;
|
|
353
|
+
return cfg;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
protected get<T>(url: string, config?: AxiosRequestConfig) {
|
|
358
|
+
return this.client.get<T>(url, config).then((res) => res.data);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
protected post<T>(url: string, data?: unknown, config?: AxiosRequestConfig) {
|
|
362
|
+
return this.client.post<T>(url, data, config).then((res) => res.data);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
protected put<T>(url: string, data?: unknown, config?: AxiosRequestConfig) {
|
|
366
|
+
return this.client.put<T>(url, data, config).then((res) => res.data);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
protected patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig) {
|
|
370
|
+
return this.client.patch<T>(url, data, config).then((res) => res.data);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
protected delete<T>(url: string, config?: AxiosRequestConfig) {
|
|
374
|
+
return this.client.delete<T>(url, config).then((res) => res.data);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Domain API Classes
|
|
380
|
+
|
|
381
|
+
Each domain gets its own class that extends `HttpClient`. If the file grows too large, split by domain — but all share the same base:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// client/UserApi.ts
|
|
385
|
+
import { HttpClient } from './HttpClient';
|
|
386
|
+
|
|
387
|
+
export class UserApi extends HttpClient {
|
|
388
|
+
constructor() {
|
|
389
|
+
super(import.meta.env.VITE_API_URL);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
getAll(params?: UserFilters) {
|
|
393
|
+
return this.get<User[]>('/users', { params });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
getById(id: string) {
|
|
397
|
+
return this.get<User>(`/users/${id}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
create(data: CreateUserDto) {
|
|
401
|
+
return this.post<User>('/users', data);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
update(id: string, data: UpdateUserDto) {
|
|
405
|
+
return this.patch<User>(`/users/${id}`, data);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
remove(id: string) {
|
|
409
|
+
return this.delete<void>(`/users/${id}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// client/OrderApi.ts
|
|
414
|
+
import { HttpClient } from './HttpClient';
|
|
415
|
+
|
|
416
|
+
export class OrderApi extends HttpClient {
|
|
417
|
+
constructor() {
|
|
418
|
+
super(import.meta.env.VITE_API_URL);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
getAll(params?: OrderFilters) {
|
|
422
|
+
return this.get<Order[]>('/orders', { params });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
getById(id: string) {
|
|
426
|
+
return this.get<Order>(`/orders/${id}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
cancel(id: string) {
|
|
430
|
+
return this.post<Order>(`/orders/${id}/cancel`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
// client/index.ts — singleton instances
|
|
437
|
+
import { UserApi } from './UserApi';
|
|
438
|
+
import { OrderApi } from './OrderApi';
|
|
439
|
+
|
|
440
|
+
export const userApi = new UserApi();
|
|
441
|
+
export const orderApi = new OrderApi();
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### SWR Hooks (consume API classes)
|
|
445
|
+
|
|
446
|
+
SWR hooks call the API class methods — no raw URLs or fetchers scattered around:
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
// hooks/useUsers.ts
|
|
450
|
+
import useSWR from 'swr';
|
|
451
|
+
import useSWRMutation from 'swr/mutation';
|
|
452
|
+
import { userApi } from '../client';
|
|
453
|
+
|
|
454
|
+
export function useUsers(filters?: UserFilters) {
|
|
455
|
+
return useSWR(['users', filters], () => userApi.getAll(filters));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function useUser(id: string | null) {
|
|
459
|
+
return useSWR(id ? ['users', id] : null, () => userApi.getById(id!));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function useCreateUser() {
|
|
463
|
+
return useSWRMutation('users', (_, { arg }: { arg: CreateUserDto }) =>
|
|
464
|
+
userApi.create(arg),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Rules:
|
|
470
|
+
- Every backend communication goes through a domain API class — no raw `axios.get()` calls anywhere in components or hooks
|
|
471
|
+
- `HttpClient` is the single source of Axios configuration (base URL, interceptors, auth headers)
|
|
472
|
+
- Split API classes by domain when a single class exceeds ~150 lines
|
|
473
|
+
- SWR hooks consume API class methods — they never construct URLs or call Axios directly
|
|
474
|
+
- SWR handles all server data caching — never duplicate server data in Zustand
|
|
475
|
+
- Mutations use `useSWRMutation` or API class methods + `mutate()` for cache invalidation
|
|
476
|
+
- SWR keys use descriptive arrays (`['users', id]`) for precise cache invalidation
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## Backend Architecture
|
|
481
|
+
|
|
482
|
+
### Express
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// routes/userRoutes.ts
|
|
486
|
+
import { Router } from 'express';
|
|
487
|
+
import { UserController } from '../controllers/userController';
|
|
488
|
+
import { authenticate } from '../middlewares/auth';
|
|
489
|
+
import { validate } from '../middlewares/validate';
|
|
490
|
+
import { createUserSchema } from '../models/schemas';
|
|
491
|
+
|
|
492
|
+
const router = Router();
|
|
493
|
+
|
|
494
|
+
router.get('/', authenticate, UserController.list);
|
|
495
|
+
router.post('/', authenticate, validate(createUserSchema), UserController.create);
|
|
496
|
+
|
|
497
|
+
export default router;
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
// controllers/userController.ts
|
|
502
|
+
export class UserController {
|
|
503
|
+
static async list(req: Request, res: Response, next: NextFunction) {
|
|
504
|
+
try {
|
|
505
|
+
const users = await UserService.findAll(req.query);
|
|
506
|
+
res.json(users);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
next(error);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Hono
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
// routes/userRoutes.ts
|
|
518
|
+
import { Hono } from 'hono';
|
|
519
|
+
import { UserHandler } from '../handlers/userHandler';
|
|
520
|
+
import { authenticate } from '../middlewares/auth';
|
|
521
|
+
import { zValidator } from '@hono/zod-validator';
|
|
522
|
+
import { createUserSchema } from '../models/schemas';
|
|
523
|
+
|
|
524
|
+
const app = new Hono();
|
|
525
|
+
|
|
526
|
+
app.get('/', authenticate, UserHandler.list);
|
|
527
|
+
app.post('/', authenticate, zValidator('json', createUserSchema), UserHandler.create);
|
|
528
|
+
|
|
529
|
+
export default app;
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Service Layer
|
|
533
|
+
|
|
534
|
+
Services contain pure business logic — no HTTP concepts (req, res). This makes them testable and reusable:
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
// services/userService.ts
|
|
538
|
+
export class UserService {
|
|
539
|
+
static async findAll(filters: UserFilters) {
|
|
540
|
+
return prisma.user.findMany({ where: filters });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
static async create(data: CreateUserDto) {
|
|
544
|
+
const existing = await prisma.user.findUnique({ where: { email: data.email } });
|
|
545
|
+
if (existing) throw new ConflictError('Email already in use');
|
|
546
|
+
return prisma.user.create({ data });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## API Response Format (RFC 7807)
|
|
554
|
+
|
|
555
|
+
Follow HTTP standards. No wrappers on success — the status code tells the story.
|
|
556
|
+
|
|
557
|
+
### Success Responses
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
// 200 OK — single resource
|
|
561
|
+
res.json({ id: 1, name: 'John', email: 'john@example.com' });
|
|
562
|
+
|
|
563
|
+
// 200 OK — paginated list
|
|
564
|
+
// Headers: X-Total-Count, X-Page, X-Per-Page, Link
|
|
565
|
+
res.set({
|
|
566
|
+
'X-Total-Count': String(total),
|
|
567
|
+
'X-Page': String(page),
|
|
568
|
+
'X-Per-Page': String(perPage),
|
|
569
|
+
}).json(users);
|
|
570
|
+
|
|
571
|
+
// 201 Created
|
|
572
|
+
res.status(201).json(createdUser);
|
|
573
|
+
|
|
574
|
+
// 204 No Content (delete)
|
|
575
|
+
res.status(204).end();
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### Error Responses (RFC 7807 Problem Details)
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
// 400 Bad Request — validation error
|
|
582
|
+
{
|
|
583
|
+
"type": "https://api.example.com/errors/validation",
|
|
584
|
+
"title": "Validation Failed",
|
|
585
|
+
"status": 400,
|
|
586
|
+
"detail": "Request body contains invalid fields",
|
|
587
|
+
"errors": [
|
|
588
|
+
{ "field": "email", "message": "Invalid email format" }
|
|
589
|
+
]
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// 401 Unauthorized
|
|
593
|
+
{
|
|
594
|
+
"type": "https://api.example.com/errors/unauthorized",
|
|
595
|
+
"title": "Unauthorized",
|
|
596
|
+
"status": 401,
|
|
597
|
+
"detail": "Invalid or expired token"
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// 404 Not Found
|
|
601
|
+
{
|
|
602
|
+
"type": "https://api.example.com/errors/not-found",
|
|
603
|
+
"title": "Not Found",
|
|
604
|
+
"status": 404,
|
|
605
|
+
"detail": "User with id 42 not found"
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 500 Internal Server Error (production — no details leaked)
|
|
609
|
+
{
|
|
610
|
+
"type": "https://api.example.com/errors/internal",
|
|
611
|
+
"title": "Internal Server Error",
|
|
612
|
+
"status": 500,
|
|
613
|
+
"detail": "An unexpected error occurred"
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## Error Handling
|
|
620
|
+
|
|
621
|
+
### Backend — Error Class Hierarchy
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
export class AppError extends Error {
|
|
625
|
+
constructor(
|
|
626
|
+
public status: number,
|
|
627
|
+
message: string,
|
|
628
|
+
public type: string,
|
|
629
|
+
public isOperational = true,
|
|
630
|
+
) {
|
|
631
|
+
super(message);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export class NotFoundError extends AppError {
|
|
636
|
+
constructor(detail = 'Resource not found') {
|
|
637
|
+
super(404, detail, 'not-found');
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export class ValidationError extends AppError {
|
|
642
|
+
constructor(detail: string, public errors: FieldError[] = []) {
|
|
643
|
+
super(400, detail, 'validation');
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export class UnauthorizedError extends AppError {
|
|
648
|
+
constructor(detail = 'Invalid or expired token') {
|
|
649
|
+
super(401, detail, 'unauthorized');
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export class ConflictError extends AppError {
|
|
654
|
+
constructor(detail: string) {
|
|
655
|
+
super(409, detail, 'conflict');
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### Backend — Global Error Middleware
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
const errorHandler: ErrorRequestHandler = (err, req, res, _next) => {
|
|
664
|
+
if (err instanceof AppError) {
|
|
665
|
+
return res.status(err.status).json({
|
|
666
|
+
type: `https://api.example.com/errors/${err.type}`,
|
|
667
|
+
title: err.message,
|
|
668
|
+
status: err.status,
|
|
669
|
+
detail: err.message,
|
|
670
|
+
...(err instanceof ValidationError && { errors: err.errors }),
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Unexpected error — log it, don't expose details
|
|
675
|
+
logger.error(err);
|
|
676
|
+
res.status(500).json({
|
|
677
|
+
type: 'https://api.example.com/errors/internal',
|
|
678
|
+
title: 'Internal Server Error',
|
|
679
|
+
status: 500,
|
|
680
|
+
detail: 'An unexpected error occurred',
|
|
681
|
+
});
|
|
682
|
+
};
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Frontend — Error Boundaries + Toast
|
|
686
|
+
|
|
687
|
+
- Wrap route-level components with `ErrorBoundary` to catch render errors
|
|
688
|
+
- Use toast notifications for async operation failures (API errors, form submissions)
|
|
689
|
+
- Never swallow errors silently — always inform the user
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## Environment Variables
|
|
694
|
+
|
|
695
|
+
Validate all environment variables at startup using Zod:
|
|
696
|
+
|
|
697
|
+
```typescript
|
|
698
|
+
// config/env.ts
|
|
699
|
+
import { z } from 'zod';
|
|
700
|
+
|
|
701
|
+
const envSchema = z.object({
|
|
702
|
+
DATABASE_URL: z.string().url(),
|
|
703
|
+
JWT_SECRET: z.string().min(32),
|
|
704
|
+
PORT: z.coerce.number().default(3000),
|
|
705
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
export const env = envSchema.parse(process.env);
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
If validation fails, the app crashes immediately at startup with a clear error message — not silently at runtime when the variable is first used.
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## Testing Standards
|
|
716
|
+
|
|
717
|
+
### Vitest (Unit & Integration)
|
|
718
|
+
|
|
719
|
+
- Test files live next to the code they test: `Button.test.tsx` alongside `Button.tsx`
|
|
720
|
+
- Test behavior, not implementation — test what the user sees and does
|
|
721
|
+
- Use `describe` / `it` blocks with readable descriptions
|
|
722
|
+
- Mock external dependencies (API calls, databases), not internal modules
|
|
723
|
+
|
|
724
|
+
### Playwright (E2E)
|
|
725
|
+
|
|
726
|
+
- E2E tests live in the app's root: `apps/<project>/e2e/`
|
|
727
|
+
- Test critical user flows: login, checkout, form submissions
|
|
728
|
+
- Use page object pattern for maintainability
|
|
729
|
+
- Never rely on implementation details (CSS classes, test IDs only)
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## Git Conventions
|
|
734
|
+
|
|
735
|
+
### Trunk-Based Development
|
|
736
|
+
|
|
737
|
+
- `main` is always deployable
|
|
738
|
+
- Short-lived feature branches: `feat/user-auth`, `fix/cart-total`
|
|
739
|
+
- No long-lived branches — merge within 1-2 days
|
|
740
|
+
- Use feature flags for incomplete features that need to be merged
|
|
741
|
+
|
|
742
|
+
### Conventional Commits
|
|
743
|
+
|
|
744
|
+
```
|
|
745
|
+
<type>(<scope>): <description>
|
|
746
|
+
|
|
747
|
+
feat(auth): implement JWT refresh token rotation
|
|
748
|
+
fix(cart): correct total calculation with discount codes
|
|
749
|
+
refactor(api): extract user validation to middleware
|
|
750
|
+
docs(readme): add deployment instructions
|
|
751
|
+
test(orders): add integration tests for order creation
|
|
752
|
+
chore(deps): update prisma to v6
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
Types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`, `build`
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
## Best Practices Checklist
|
|
760
|
+
|
|
761
|
+
Apply these on every piece of code:
|
|
762
|
+
|
|
763
|
+
### TypeScript
|
|
764
|
+
- Enable `strict` mode — no exceptions
|
|
765
|
+
- No `any` — use `unknown` and narrow with type guards
|
|
766
|
+
- Prefer `interface` for object shapes, `type` for unions/intersections
|
|
767
|
+
- Use discriminated unions for state machines and variants
|
|
768
|
+
- Define return types explicitly on exported functions
|
|
769
|
+
|
|
770
|
+
### React (Web & Mobile)
|
|
771
|
+
- Functional components only — no class components
|
|
772
|
+
- Custom hooks for reusable logic — prefix with `use`
|
|
773
|
+
- Memoize expensive computations with `useMemo`, not everything
|
|
774
|
+
- Use `React.lazy` + `Suspense` for code splitting (web)
|
|
775
|
+
- Avoid prop drilling — use composition or context for deep trees
|
|
776
|
+
- Keys must be stable, unique identifiers — never array index
|
|
777
|
+
|
|
778
|
+
### Performance
|
|
779
|
+
- Lazy load routes and heavy components
|
|
780
|
+
- Virtualize long lists (`react-window` or `FlashList` for mobile)
|
|
781
|
+
- Debounce search inputs and other frequent events
|
|
782
|
+
- Use `loading` and `error` states from SWR — no manual loading booleans
|
|
783
|
+
|
|
784
|
+
### Security
|
|
785
|
+
- Sanitize all user input — never trust client data
|
|
786
|
+
- Parameterized queries only (Prisma handles this)
|
|
787
|
+
- CORS configured per environment
|
|
788
|
+
- Rate limiting on auth endpoints
|
|
789
|
+
- Never store secrets in client code
|
|
790
|
+
- Validate request bodies with Zod schemas before processing
|
|
791
|
+
|
|
792
|
+
---
|
|
793
|
+
|
|
794
|
+
## i18n (Optional — Ask Per Project)
|
|
795
|
+
|
|
796
|
+
When i18n is needed, use `react-i18next`:
|
|
797
|
+
|
|
798
|
+
```
|
|
799
|
+
src/
|
|
800
|
+
└── locales/
|
|
801
|
+
├── en/
|
|
802
|
+
│ └── translation.json
|
|
803
|
+
└── tr/
|
|
804
|
+
└── translation.json
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
Rules:
|
|
808
|
+
- All user-facing strings must use translation keys
|
|
809
|
+
- Never hardcode text in components
|
|
810
|
+
- Use namespaces for large apps to split translation files
|