@aicgen/aicgen 1.0.0 → 1.0.2
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/AGENTS.md +7 -2
- package/claude.md +1 -1
- package/dist/index.js +6272 -5503
- package/package.json +1 -1
- package/.agent/rules/api-design.md +0 -649
- package/.agent/rules/architecture.md +0 -2507
- package/.agent/rules/best-practices.md +0 -622
- package/.agent/rules/code-style.md +0 -308
- package/.agent/rules/design-patterns.md +0 -577
- package/.agent/rules/devops.md +0 -230
- package/.agent/rules/error-handling.md +0 -417
- package/.agent/rules/instructions.md +0 -28
- package/.agent/rules/language.md +0 -786
- package/.agent/rules/performance.md +0 -710
- package/.agent/rules/security.md +0 -587
- package/.agent/rules/testing.md +0 -572
- package/.agent/workflows/add-documentation.md +0 -10
- package/.agent/workflows/generate-integration-tests.md +0 -10
- package/.agent/workflows/generate-unit-tests.md +0 -11
- package/.agent/workflows/performance-audit.md +0 -11
- package/.agent/workflows/refactor-extract-module.md +0 -12
- package/.agent/workflows/security-audit.md +0 -12
- package/.gemini/instructions.md +0 -4843
package/.gemini/instructions.md
DELETED
|
@@ -1,4843 +0,0 @@
|
|
|
1
|
-
# Gemini Development Guide
|
|
2
|
-
|
|
3
|
-
**Role:** You are an expert software engineer specialized in typescript.
|
|
4
|
-
**Objective:** Write clean, efficient, and maintainable code following the guidelines below.
|
|
5
|
-
|
|
6
|
-
## Guiding Principles
|
|
7
|
-
1. **Quality First**: Prioritize code quality and maintainability over speed.
|
|
8
|
-
2. **Step-by-Step**: Think through problems systematically.
|
|
9
|
-
3. **Verify**: Double-check your code against the guidelines.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Language
|
|
14
|
-
|
|
15
|
-
# TypeScript Fundamentals
|
|
16
|
-
|
|
17
|
-
## Strict Mode (Required)
|
|
18
|
-
|
|
19
|
-
Always use strict mode in `tsconfig.json`:
|
|
20
|
-
|
|
21
|
-
```json
|
|
22
|
-
{
|
|
23
|
-
"compilerOptions": {
|
|
24
|
-
"strict": true,
|
|
25
|
-
"noImplicitAny": true,
|
|
26
|
-
"strictNullChecks": true,
|
|
27
|
-
"strictFunctionTypes": true
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Type Annotations
|
|
33
|
-
|
|
34
|
-
Use explicit types for clarity:
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
// Function signatures
|
|
38
|
-
function calculateTotal(items: CartItem[], taxRate: number): number {
|
|
39
|
-
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
|
|
40
|
-
return subtotal * (1 + taxRate);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Variable declarations
|
|
44
|
-
const userName: string = "Alice";
|
|
45
|
-
const age: number = 30;
|
|
46
|
-
const isActive: boolean = true;
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Avoid `any`
|
|
50
|
-
|
|
51
|
-
Never use `any` - use `unknown` with type guards:
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
// ❌ Bad
|
|
55
|
-
function processData(data: any) {
|
|
56
|
-
return data.value;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ✅ Good
|
|
60
|
-
function processData(data: unknown): string {
|
|
61
|
-
if (typeof data === 'object' && data !== null && 'value' in data) {
|
|
62
|
-
return String(data.value);
|
|
63
|
-
}
|
|
64
|
-
throw new Error('Invalid data structure');
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Type Guards
|
|
69
|
-
|
|
70
|
-
Implement custom type guards:
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
interface User {
|
|
74
|
-
id: string;
|
|
75
|
-
email: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function isUser(value: unknown): value is User {
|
|
79
|
-
return (
|
|
80
|
-
typeof value === 'object' &&
|
|
81
|
-
value !== null &&
|
|
82
|
-
'id' in value &&
|
|
83
|
-
'email' in value &&
|
|
84
|
-
typeof value.id === 'string' &&
|
|
85
|
-
typeof value.email === 'string'
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Usage
|
|
90
|
-
if (isUser(data)) {
|
|
91
|
-
console.log(data.email); // Type: User
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Naming Conventions
|
|
96
|
-
|
|
97
|
-
- Classes/Interfaces: `PascalCase`
|
|
98
|
-
- Functions/Variables: `camelCase`
|
|
99
|
-
- Constants: `UPPER_SNAKE_CASE`
|
|
100
|
-
- Files: `kebab-case.ts`
|
|
101
|
-
- No `I` prefix for interfaces
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# Async/Await Patterns
|
|
105
|
-
|
|
106
|
-
## Prefer async/await
|
|
107
|
-
|
|
108
|
-
Always use async/await over promise chains:
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
// ✅ Good
|
|
112
|
-
async function fetchUser(id: string): Promise<User> {
|
|
113
|
-
const response = await fetch(`/api/users/${id}`);
|
|
114
|
-
if (!response.ok) {
|
|
115
|
-
throw new Error(`HTTP ${response.status}`);
|
|
116
|
-
}
|
|
117
|
-
return await response.json();
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ❌ Avoid
|
|
121
|
-
function fetchUser(id: string): Promise<User> {
|
|
122
|
-
return fetch(`/api/users/${id}`)
|
|
123
|
-
.then(res => res.json());
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## Error Handling
|
|
128
|
-
|
|
129
|
-
Always wrap async operations in try/catch:
|
|
130
|
-
|
|
131
|
-
```typescript
|
|
132
|
-
async function safeOperation(): Promise<Result> {
|
|
133
|
-
try {
|
|
134
|
-
const data = await riskyOperation();
|
|
135
|
-
return { success: true, data };
|
|
136
|
-
} catch (error) {
|
|
137
|
-
logger.error('Operation failed', error);
|
|
138
|
-
return { success: false, error: error.message };
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## Parallel Execution
|
|
144
|
-
|
|
145
|
-
Use `Promise.all()` for independent operations:
|
|
146
|
-
|
|
147
|
-
```typescript
|
|
148
|
-
// ✅ Good - parallel (fast)
|
|
149
|
-
const [users, posts, comments] = await Promise.all([
|
|
150
|
-
fetchUsers(),
|
|
151
|
-
fetchPosts(),
|
|
152
|
-
fetchComments()
|
|
153
|
-
]);
|
|
154
|
-
|
|
155
|
-
// ❌ Bad - sequential (slow)
|
|
156
|
-
const users = await fetchUsers();
|
|
157
|
-
const posts = await fetchPosts();
|
|
158
|
-
const comments = await fetchComments();
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
## Handling Failures
|
|
162
|
-
|
|
163
|
-
Use `Promise.allSettled()` when some failures are acceptable:
|
|
164
|
-
|
|
165
|
-
```typescript
|
|
166
|
-
const results = await Promise.allSettled([
|
|
167
|
-
fetchData1(),
|
|
168
|
-
fetchData2(),
|
|
169
|
-
fetchData3()
|
|
170
|
-
]);
|
|
171
|
-
|
|
172
|
-
results.forEach((result, index) => {
|
|
173
|
-
if (result.status === 'fulfilled') {
|
|
174
|
-
console.log(`Success ${index}:`, result.value);
|
|
175
|
-
} else {
|
|
176
|
-
console.error(`Failed ${index}:`, result.reason);
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
## Retry Pattern
|
|
182
|
-
|
|
183
|
-
Implement retry with exponential backoff:
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
async function retryWithBackoff<T>(
|
|
187
|
-
fn: () => Promise<T>,
|
|
188
|
-
maxRetries: number = 3
|
|
189
|
-
): Promise<T> {
|
|
190
|
-
let lastError: Error;
|
|
191
|
-
|
|
192
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
193
|
-
try {
|
|
194
|
-
return await fn();
|
|
195
|
-
} catch (error) {
|
|
196
|
-
lastError = error as Error;
|
|
197
|
-
if (attempt < maxRetries - 1) {
|
|
198
|
-
const delay = 1000 * Math.pow(2, attempt);
|
|
199
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
throw lastError!;
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
# TypeScript Types & Interfaces
|
|
210
|
-
|
|
211
|
-
## Prefer Interfaces for Public APIs
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
// ✅ Use interfaces for object shapes
|
|
215
|
-
interface User {
|
|
216
|
-
id: string;
|
|
217
|
-
name: string;
|
|
218
|
-
email: string;
|
|
219
|
-
createdAt: Date;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// ✅ Use type aliases for unions and complex types
|
|
223
|
-
type UserRole = 'admin' | 'editor' | 'viewer';
|
|
224
|
-
type ResponseHandler = (response: Response) => void;
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
## Discriminated Unions
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
// ✅ Use discriminated unions for variant types
|
|
231
|
-
type Result<T> =
|
|
232
|
-
| { success: true; data: T }
|
|
233
|
-
| { success: false; error: string };
|
|
234
|
-
|
|
235
|
-
function handleResult(result: Result<User>) {
|
|
236
|
-
if (result.success) {
|
|
237
|
-
console.log(result.data.name); // TypeScript knows data exists
|
|
238
|
-
} else {
|
|
239
|
-
console.error(result.error); // TypeScript knows error exists
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
## Utility Types
|
|
245
|
-
|
|
246
|
-
```typescript
|
|
247
|
-
// Use built-in utility types
|
|
248
|
-
type PartialUser = Partial<User>; // All fields optional
|
|
249
|
-
type RequiredUser = Required<User>; // All fields required
|
|
250
|
-
type ReadonlyUser = Readonly<User>; // All fields readonly
|
|
251
|
-
type UserKeys = keyof User; // 'id' | 'name' | 'email' | 'createdAt'
|
|
252
|
-
type PickedUser = Pick<User, 'id' | 'name'>; // Only id and name
|
|
253
|
-
type OmittedUser = Omit<User, 'createdAt'>; // Everything except createdAt
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
## Type Guards
|
|
257
|
-
|
|
258
|
-
```typescript
|
|
259
|
-
// ✅ Use type guards for runtime checking
|
|
260
|
-
function isUser(value: unknown): value is User {
|
|
261
|
-
return (
|
|
262
|
-
typeof value === 'object' &&
|
|
263
|
-
value !== null &&
|
|
264
|
-
'id' in value &&
|
|
265
|
-
'email' in value
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Usage
|
|
270
|
-
const data: unknown = fetchData();
|
|
271
|
-
if (isUser(data)) {
|
|
272
|
-
console.log(data.email); // TypeScript knows it's a User
|
|
273
|
-
}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
## Avoid `any`
|
|
277
|
-
|
|
278
|
-
```typescript
|
|
279
|
-
// ❌ Never use any
|
|
280
|
-
function process(data: any) {
|
|
281
|
-
return data.name; // No type safety
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// ✅ Use unknown with type guards
|
|
285
|
-
function process(data: unknown) {
|
|
286
|
-
if (isUser(data)) {
|
|
287
|
-
return data.name; // Type-safe
|
|
288
|
-
}
|
|
289
|
-
throw new Error('Invalid data');
|
|
290
|
-
}
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
# TypeScript Error Handling
|
|
295
|
-
|
|
296
|
-
## Custom Error Classes
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
// ✅ Create structured error hierarchy
|
|
300
|
-
class AppError extends Error {
|
|
301
|
-
constructor(
|
|
302
|
-
message: string,
|
|
303
|
-
public statusCode: number = 500,
|
|
304
|
-
public code: string = 'INTERNAL_ERROR',
|
|
305
|
-
public details?: unknown
|
|
306
|
-
) {
|
|
307
|
-
super(message);
|
|
308
|
-
this.name = this.constructor.name;
|
|
309
|
-
Error.captureStackTrace(this, this.constructor);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
class NotFoundError extends AppError {
|
|
314
|
-
constructor(resource: string, id: string) {
|
|
315
|
-
super(`${resource} with id ${id} not found`, 404, 'NOT_FOUND', { resource, id });
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
class ValidationError extends AppError {
|
|
320
|
-
constructor(message: string, details: unknown) {
|
|
321
|
-
super(message, 400, 'VALIDATION_ERROR', details);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
## Async Error Handling
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
// ✅ Always handle promise rejections
|
|
330
|
-
async function fetchUser(id: string): Promise<User> {
|
|
331
|
-
try {
|
|
332
|
-
const response = await api.get(`/users/${id}`);
|
|
333
|
-
return response.data;
|
|
334
|
-
} catch (error) {
|
|
335
|
-
if (error instanceof ApiError && error.status === 404) {
|
|
336
|
-
throw new NotFoundError('User', id);
|
|
337
|
-
}
|
|
338
|
-
throw new AppError('Failed to fetch user', 500, 'FETCH_ERROR', { userId: id });
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// ✅ Use wrapper for Express async handlers
|
|
343
|
-
const asyncHandler = (fn: RequestHandler) => {
|
|
344
|
-
return (req: Request, res: Response, next: NextFunction) => {
|
|
345
|
-
Promise.resolve(fn(req, res, next)).catch(next);
|
|
346
|
-
};
|
|
347
|
-
};
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
## Result Type Pattern
|
|
351
|
-
|
|
352
|
-
```typescript
|
|
353
|
-
// ✅ Explicit success/failure without exceptions
|
|
354
|
-
type Result<T, E = Error> =
|
|
355
|
-
| { success: true; value: T }
|
|
356
|
-
| { success: false; error: E };
|
|
357
|
-
|
|
358
|
-
function parseJSON<T>(json: string): Result<T, string> {
|
|
359
|
-
try {
|
|
360
|
-
return { success: true, value: JSON.parse(json) };
|
|
361
|
-
} catch {
|
|
362
|
-
return { success: false, error: 'Invalid JSON' };
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Usage
|
|
367
|
-
const result = parseJSON<User>(data);
|
|
368
|
-
if (result.success) {
|
|
369
|
-
console.log(result.value.name);
|
|
370
|
-
} else {
|
|
371
|
-
console.error(result.error);
|
|
372
|
-
}
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
## Centralized Error Handler
|
|
376
|
-
|
|
377
|
-
```typescript
|
|
378
|
-
// ✅ Express error middleware
|
|
379
|
-
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
|
380
|
-
if (err instanceof AppError) {
|
|
381
|
-
return res.status(err.statusCode).json({
|
|
382
|
-
error: { message: err.message, code: err.code, details: err.details }
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
console.error('Unexpected error:', err);
|
|
387
|
-
res.status(500).json({
|
|
388
|
-
error: { message: 'Internal server error', code: 'INTERNAL_ERROR' }
|
|
389
|
-
});
|
|
390
|
-
});
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
# TypeScript Testing
|
|
395
|
-
|
|
396
|
-
## Test Structure: Arrange-Act-Assert
|
|
397
|
-
|
|
398
|
-
```typescript
|
|
399
|
-
describe('UserService', () => {
|
|
400
|
-
describe('createUser', () => {
|
|
401
|
-
it('should create user with hashed password', async () => {
|
|
402
|
-
// Arrange
|
|
403
|
-
const userData = { email: 'test@example.com', password: 'password123' };
|
|
404
|
-
const mockRepo = { save: jest.fn().mockResolvedValue({ id: '1', ...userData }) };
|
|
405
|
-
const service = new UserService(mockRepo);
|
|
406
|
-
|
|
407
|
-
// Act
|
|
408
|
-
const result = await service.createUser(userData);
|
|
409
|
-
|
|
410
|
-
// Assert
|
|
411
|
-
expect(result.id).toBe('1');
|
|
412
|
-
expect(mockRepo.save).toHaveBeenCalledWith(
|
|
413
|
-
expect.objectContaining({ email: 'test@example.com' })
|
|
414
|
-
);
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
});
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
## Test Observable Behavior, Not Implementation
|
|
421
|
-
|
|
422
|
-
```typescript
|
|
423
|
-
// ❌ Testing implementation details
|
|
424
|
-
it('should call validateEmail method', () => {
|
|
425
|
-
const spy = jest.spyOn(service, 'validateEmail');
|
|
426
|
-
service.createUser({ email: 'test@example.com' });
|
|
427
|
-
expect(spy).toHaveBeenCalled(); // Brittle - breaks if refactored
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
// ✅ Testing observable behavior
|
|
431
|
-
it('should reject invalid email', async () => {
|
|
432
|
-
await expect(
|
|
433
|
-
service.createUser({ email: 'invalid' })
|
|
434
|
-
).rejects.toThrow('Invalid email');
|
|
435
|
-
});
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
## Test Doubles
|
|
439
|
-
|
|
440
|
-
```typescript
|
|
441
|
-
// Stub: Returns canned responses
|
|
442
|
-
const stubDatabase = {
|
|
443
|
-
findUser: () => ({ id: '1', name: 'Test User' })
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
// Mock: Pre-programmed with expectations
|
|
447
|
-
const mockPayment = {
|
|
448
|
-
charge: jest.fn()
|
|
449
|
-
.mockResolvedValueOnce({ success: true })
|
|
450
|
-
.mockResolvedValueOnce({ success: false })
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
// Fake: Working implementation (not for production)
|
|
454
|
-
class FakeDatabase implements Database {
|
|
455
|
-
private data = new Map<string, any>();
|
|
456
|
-
|
|
457
|
-
async save(id: string, data: any) { this.data.set(id, data); }
|
|
458
|
-
async find(id: string) { return this.data.get(id); }
|
|
459
|
-
}
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
## One Test Per Condition
|
|
463
|
-
|
|
464
|
-
```typescript
|
|
465
|
-
// ❌ Multiple assertions for different scenarios
|
|
466
|
-
it('should validate user input', () => {
|
|
467
|
-
expect(() => validate({ age: -1 })).toThrow();
|
|
468
|
-
expect(() => validate({ age: 200 })).toThrow();
|
|
469
|
-
expect(() => validate({ name: '' })).toThrow();
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
// ✅ One test per condition
|
|
473
|
-
it('should reject negative age', () => {
|
|
474
|
-
expect(() => validate({ age: -1 })).toThrow('Age must be positive');
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it('should reject age over 150', () => {
|
|
478
|
-
expect(() => validate({ age: 200 })).toThrow('Age must be under 150');
|
|
479
|
-
});
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
## Keep Tests Independent
|
|
483
|
-
|
|
484
|
-
```typescript
|
|
485
|
-
// ✅ Each test is self-contained
|
|
486
|
-
it('should update user', async () => {
|
|
487
|
-
const user = await service.createUser({ name: 'Test' });
|
|
488
|
-
const updated = await service.updateUser(user.id, { name: 'Updated' });
|
|
489
|
-
expect(updated.name).toBe('Updated');
|
|
490
|
-
});
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
# TypeScript Configuration
|
|
495
|
-
|
|
496
|
-
## tsconfig.json Best Practices
|
|
497
|
-
|
|
498
|
-
```json
|
|
499
|
-
{
|
|
500
|
-
"compilerOptions": {
|
|
501
|
-
// Strict type checking
|
|
502
|
-
"strict": true,
|
|
503
|
-
"noImplicitAny": true,
|
|
504
|
-
"strictNullChecks": true,
|
|
505
|
-
"strictFunctionTypes": true,
|
|
506
|
-
"strictBindCallApply": true,
|
|
507
|
-
"strictPropertyInitialization": true,
|
|
508
|
-
"noImplicitThis": true,
|
|
509
|
-
"alwaysStrict": true,
|
|
510
|
-
|
|
511
|
-
// Additional checks
|
|
512
|
-
"noUnusedLocals": true,
|
|
513
|
-
"noUnusedParameters": true,
|
|
514
|
-
"noImplicitReturns": true,
|
|
515
|
-
"noFallthroughCasesInSwitch": true,
|
|
516
|
-
"noUncheckedIndexedAccess": true,
|
|
517
|
-
|
|
518
|
-
// Module resolution
|
|
519
|
-
"module": "ESNext",
|
|
520
|
-
"moduleResolution": "bundler",
|
|
521
|
-
"esModuleInterop": true,
|
|
522
|
-
"allowSyntheticDefaultImports": true,
|
|
523
|
-
"resolveJsonModule": true,
|
|
524
|
-
|
|
525
|
-
// Output
|
|
526
|
-
"target": "ES2022",
|
|
527
|
-
"outDir": "./dist",
|
|
528
|
-
"declaration": true,
|
|
529
|
-
"declarationMap": true,
|
|
530
|
-
"sourceMap": true,
|
|
531
|
-
|
|
532
|
-
// Path aliases
|
|
533
|
-
"baseUrl": ".",
|
|
534
|
-
"paths": {
|
|
535
|
-
"@/*": ["src/*"],
|
|
536
|
-
"@services/*": ["src/services/*"],
|
|
537
|
-
"@models/*": ["src/models/*"]
|
|
538
|
-
}
|
|
539
|
-
},
|
|
540
|
-
"include": ["src/**/*"],
|
|
541
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
542
|
-
}
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
## Path Aliases Setup
|
|
546
|
-
|
|
547
|
-
```typescript
|
|
548
|
-
// With path aliases configured:
|
|
549
|
-
import { UserService } from '@services/user';
|
|
550
|
-
import { User } from '@models/user';
|
|
551
|
-
|
|
552
|
-
// Instead of relative paths:
|
|
553
|
-
import { UserService } from '../../../services/user';
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
## Project References (Monorepo)
|
|
557
|
-
|
|
558
|
-
```json
|
|
559
|
-
// packages/shared/tsconfig.json
|
|
560
|
-
{
|
|
561
|
-
"compilerOptions": {
|
|
562
|
-
"composite": true,
|
|
563
|
-
"outDir": "./dist"
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// packages/api/tsconfig.json
|
|
568
|
-
{
|
|
569
|
-
"extends": "../../tsconfig.base.json",
|
|
570
|
-
"references": [
|
|
571
|
-
{ "path": "../shared" }
|
|
572
|
-
]
|
|
573
|
-
}
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
## Environment-Specific Configs
|
|
577
|
-
|
|
578
|
-
```json
|
|
579
|
-
// tsconfig.build.json - for production builds
|
|
580
|
-
{
|
|
581
|
-
"extends": "./tsconfig.json",
|
|
582
|
-
"compilerOptions": {
|
|
583
|
-
"sourceMap": false,
|
|
584
|
-
"removeComments": true
|
|
585
|
-
},
|
|
586
|
-
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
|
|
587
|
-
}
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
---
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
## Testing
|
|
595
|
-
|
|
596
|
-
# Unit Testing Fundamentals
|
|
597
|
-
|
|
598
|
-
## Arrange-Act-Assert Pattern
|
|
599
|
-
|
|
600
|
-
```typescript
|
|
601
|
-
describe('UserService', () => {
|
|
602
|
-
it('should create user with hashed password', async () => {
|
|
603
|
-
// Arrange - Set up test data and dependencies
|
|
604
|
-
const userData = { email: 'test@example.com', password: 'secret123' };
|
|
605
|
-
const mockRepo = { save: jest.fn().mockResolvedValue({ id: '1', ...userData }) };
|
|
606
|
-
const service = new UserService(mockRepo);
|
|
607
|
-
|
|
608
|
-
// Act - Execute the behavior being tested
|
|
609
|
-
const result = await service.createUser(userData);
|
|
610
|
-
|
|
611
|
-
// Assert - Verify the outcomes
|
|
612
|
-
expect(result.id).toBe('1');
|
|
613
|
-
expect(mockRepo.save).toHaveBeenCalledWith(
|
|
614
|
-
expect.objectContaining({ email: 'test@example.com' })
|
|
615
|
-
);
|
|
616
|
-
});
|
|
617
|
-
});
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
## Test Observable Behavior, Not Implementation
|
|
621
|
-
|
|
622
|
-
```typescript
|
|
623
|
-
// ❌ Bad: Testing implementation details
|
|
624
|
-
it('should call validateEmail method', () => {
|
|
625
|
-
const spy = jest.spyOn(service, 'validateEmail');
|
|
626
|
-
service.createUser({ email: 'test@example.com' });
|
|
627
|
-
expect(spy).toHaveBeenCalled();
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
// ✅ Good: Testing observable behavior
|
|
631
|
-
it('should reject invalid email', async () => {
|
|
632
|
-
await expect(
|
|
633
|
-
service.createUser({ email: 'invalid-email' })
|
|
634
|
-
).rejects.toThrow('Invalid email format');
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it('should accept valid email', async () => {
|
|
638
|
-
const result = await service.createUser({ email: 'valid@example.com' });
|
|
639
|
-
expect(result.email).toBe('valid@example.com');
|
|
640
|
-
});
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
## One Assertion Per Test Concept
|
|
644
|
-
|
|
645
|
-
```typescript
|
|
646
|
-
// ❌ Bad: Multiple unrelated assertions
|
|
647
|
-
it('should validate user input', () => {
|
|
648
|
-
expect(() => validate({ age: -1 })).toThrow();
|
|
649
|
-
expect(() => validate({ age: 200 })).toThrow();
|
|
650
|
-
expect(() => validate({ name: '' })).toThrow();
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
// ✅ Good: One test per scenario
|
|
654
|
-
it('should reject negative age', () => {
|
|
655
|
-
expect(() => validate({ age: -1 })).toThrow('Age must be positive');
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
it('should reject age over 150', () => {
|
|
659
|
-
expect(() => validate({ age: 200 })).toThrow('Age must be under 150');
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
it('should reject empty name', () => {
|
|
663
|
-
expect(() => validate({ name: '' })).toThrow('Name is required');
|
|
664
|
-
});
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
## Descriptive Test Names
|
|
668
|
-
|
|
669
|
-
```typescript
|
|
670
|
-
// ❌ Vague names
|
|
671
|
-
it('should work correctly', () => {});
|
|
672
|
-
it('handles edge case', () => {});
|
|
673
|
-
|
|
674
|
-
// ✅ Descriptive names - describe the scenario and expected outcome
|
|
675
|
-
it('should return empty array when no users match filter', () => {});
|
|
676
|
-
it('should throw ValidationError when email is empty', () => {});
|
|
677
|
-
it('should retry failed payment up to 3 times before giving up', () => {});
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
## Tests Should Be Independent
|
|
681
|
-
|
|
682
|
-
```typescript
|
|
683
|
-
// ❌ Bad: Tests depend on each other
|
|
684
|
-
let userId: string;
|
|
685
|
-
|
|
686
|
-
it('should create user', async () => {
|
|
687
|
-
const user = await service.createUser(data);
|
|
688
|
-
userId = user.id; // Shared state!
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
it('should update user', async () => {
|
|
692
|
-
await service.updateUser(userId, newData); // Depends on previous test
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
// ✅ Good: Each test is self-contained
|
|
696
|
-
it('should update user', async () => {
|
|
697
|
-
const user = await service.createUser(data);
|
|
698
|
-
const updated = await service.updateUser(user.id, newData);
|
|
699
|
-
expect(updated.name).toBe(newData.name);
|
|
700
|
-
});
|
|
701
|
-
```
|
|
702
|
-
|
|
703
|
-
## Test Edge Cases
|
|
704
|
-
|
|
705
|
-
```typescript
|
|
706
|
-
describe('divide', () => {
|
|
707
|
-
it('should divide two positive numbers', () => {
|
|
708
|
-
expect(divide(10, 2)).toBe(5);
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
it('should throw when dividing by zero', () => {
|
|
712
|
-
expect(() => divide(10, 0)).toThrow('Division by zero');
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
it('should handle negative numbers', () => {
|
|
716
|
-
expect(divide(-10, 2)).toBe(-5);
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
it('should return zero when numerator is zero', () => {
|
|
720
|
-
expect(divide(0, 5)).toBe(0);
|
|
721
|
-
});
|
|
722
|
-
});
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
# Test Doubles and Mocking
|
|
727
|
-
|
|
728
|
-
## Types of Test Doubles
|
|
729
|
-
|
|
730
|
-
```typescript
|
|
731
|
-
// STUB: Returns canned responses
|
|
732
|
-
const stubUserRepo = {
|
|
733
|
-
findById: () => ({ id: '1', name: 'Test User' })
|
|
734
|
-
};
|
|
735
|
-
|
|
736
|
-
// MOCK: Pre-programmed with expectations
|
|
737
|
-
const mockPaymentGateway = {
|
|
738
|
-
charge: jest.fn()
|
|
739
|
-
.mockResolvedValueOnce({ success: true, transactionId: 'tx1' })
|
|
740
|
-
.mockResolvedValueOnce({ success: false, error: 'Declined' })
|
|
741
|
-
};
|
|
742
|
-
|
|
743
|
-
// SPY: Records calls for verification
|
|
744
|
-
const spy = jest.spyOn(emailService, 'send');
|
|
745
|
-
|
|
746
|
-
// FAKE: Working implementation (not for production)
|
|
747
|
-
class FakeDatabase implements Database {
|
|
748
|
-
private data = new Map<string, any>();
|
|
749
|
-
|
|
750
|
-
async save(id: string, entity: any) { this.data.set(id, entity); }
|
|
751
|
-
async find(id: string) { return this.data.get(id); }
|
|
752
|
-
}
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
## When to Mock
|
|
756
|
-
|
|
757
|
-
```typescript
|
|
758
|
-
// ✅ Mock external services (APIs, databases)
|
|
759
|
-
const mockHttpClient = {
|
|
760
|
-
get: jest.fn().mockResolvedValue({ data: userData })
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
// ✅ Mock time-dependent operations
|
|
764
|
-
jest.useFakeTimers();
|
|
765
|
-
jest.setSystemTime(new Date('2024-01-15'));
|
|
766
|
-
|
|
767
|
-
// ✅ Mock random/non-deterministic functions
|
|
768
|
-
jest.spyOn(Math, 'random').mockReturnValue(0.5);
|
|
769
|
-
|
|
770
|
-
// ❌ Don't mock the code you're testing
|
|
771
|
-
// ❌ Don't mock simple data structures
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
## Mock Verification
|
|
775
|
-
|
|
776
|
-
```typescript
|
|
777
|
-
it('should send welcome email after registration', async () => {
|
|
778
|
-
const mockEmail = { send: jest.fn().mockResolvedValue(true) };
|
|
779
|
-
const service = new UserService({ emailService: mockEmail });
|
|
780
|
-
|
|
781
|
-
await service.register({ email: 'new@example.com' });
|
|
782
|
-
|
|
783
|
-
expect(mockEmail.send).toHaveBeenCalledWith({
|
|
784
|
-
to: 'new@example.com',
|
|
785
|
-
template: 'welcome',
|
|
786
|
-
subject: 'Welcome!'
|
|
787
|
-
});
|
|
788
|
-
expect(mockEmail.send).toHaveBeenCalledTimes(1);
|
|
789
|
-
});
|
|
790
|
-
```
|
|
791
|
-
|
|
792
|
-
## Partial Mocks
|
|
793
|
-
|
|
794
|
-
```typescript
|
|
795
|
-
// Mock only specific methods
|
|
796
|
-
const service = new OrderService();
|
|
797
|
-
|
|
798
|
-
jest.spyOn(service, 'validateOrder').mockReturnValue(true);
|
|
799
|
-
jest.spyOn(service, 'calculateTotal').mockReturnValue(100);
|
|
800
|
-
// Other methods use real implementation
|
|
801
|
-
|
|
802
|
-
const result = await service.processOrder(orderData);
|
|
803
|
-
expect(result.total).toBe(100);
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
## Resetting Mocks
|
|
807
|
-
|
|
808
|
-
```typescript
|
|
809
|
-
describe('PaymentService', () => {
|
|
810
|
-
const mockGateway = { charge: jest.fn() };
|
|
811
|
-
const service = new PaymentService(mockGateway);
|
|
812
|
-
|
|
813
|
-
beforeEach(() => {
|
|
814
|
-
jest.clearAllMocks(); // Clear call history
|
|
815
|
-
// or jest.resetAllMocks() to also reset return values
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
it('should process payment', async () => {
|
|
819
|
-
mockGateway.charge.mockResolvedValue({ success: true });
|
|
820
|
-
await service.charge(100);
|
|
821
|
-
expect(mockGateway.charge).toHaveBeenCalledTimes(1);
|
|
822
|
-
});
|
|
823
|
-
});
|
|
824
|
-
```
|
|
825
|
-
|
|
826
|
-
## Mock Modules
|
|
827
|
-
|
|
828
|
-
```typescript
|
|
829
|
-
// Mock entire module
|
|
830
|
-
jest.mock('./email-service', () => ({
|
|
831
|
-
EmailService: jest.fn().mockImplementation(() => ({
|
|
832
|
-
send: jest.fn().mockResolvedValue(true)
|
|
833
|
-
}))
|
|
834
|
-
}));
|
|
835
|
-
|
|
836
|
-
// Mock with partial implementation
|
|
837
|
-
jest.mock('./config', () => ({
|
|
838
|
-
...jest.requireActual('./config'),
|
|
839
|
-
API_KEY: 'test-key'
|
|
840
|
-
}));
|
|
841
|
-
```
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
# Testing Basics
|
|
845
|
-
|
|
846
|
-
## Your First Unit Test
|
|
847
|
-
|
|
848
|
-
A unit test verifies that a small piece of code works correctly.
|
|
849
|
-
|
|
850
|
-
```pseudocode
|
|
851
|
-
// Function to test
|
|
852
|
-
function add(a, b):
|
|
853
|
-
return a + b
|
|
854
|
-
|
|
855
|
-
// Test for the function
|
|
856
|
-
test "add should sum two numbers":
|
|
857
|
-
result = add(2, 3)
|
|
858
|
-
expect result equals 5
|
|
859
|
-
```
|
|
860
|
-
|
|
861
|
-
## Test Structure
|
|
862
|
-
|
|
863
|
-
Every test has three parts:
|
|
864
|
-
|
|
865
|
-
1. **Setup** - Prepare what you need
|
|
866
|
-
2. **Execute** - Run the code
|
|
867
|
-
3. **Verify** - Check the result
|
|
868
|
-
|
|
869
|
-
```pseudocode
|
|
870
|
-
test "should create user with name":
|
|
871
|
-
// 1. Setup
|
|
872
|
-
userName = "Alice"
|
|
873
|
-
|
|
874
|
-
// 2. Execute
|
|
875
|
-
user = createUser(userName)
|
|
876
|
-
|
|
877
|
-
// 3. Verify
|
|
878
|
-
expect user.name equals "Alice"
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
## Common Assertions
|
|
882
|
-
|
|
883
|
-
```pseudocode
|
|
884
|
-
// Equality
|
|
885
|
-
expect value equals 5
|
|
886
|
-
expect value equals { id: 1 }
|
|
887
|
-
|
|
888
|
-
// Truthiness
|
|
889
|
-
expect value is truthy
|
|
890
|
-
expect value is falsy
|
|
891
|
-
expect value is null
|
|
892
|
-
expect value is undefined
|
|
893
|
-
|
|
894
|
-
// Numbers
|
|
895
|
-
expect value > 3
|
|
896
|
-
expect value < 10
|
|
897
|
-
|
|
898
|
-
// Strings
|
|
899
|
-
expect text contains "hello"
|
|
900
|
-
|
|
901
|
-
// Arrays/Lists
|
|
902
|
-
expect array contains item
|
|
903
|
-
expect array length equals 3
|
|
904
|
-
```
|
|
905
|
-
|
|
906
|
-
## Testing Expected Errors
|
|
907
|
-
|
|
908
|
-
```pseudocode
|
|
909
|
-
test "should throw error for invalid input":
|
|
910
|
-
expect error when:
|
|
911
|
-
divide(10, 0)
|
|
912
|
-
with message "Cannot divide by zero"
|
|
913
|
-
```
|
|
914
|
-
|
|
915
|
-
## Async Tests
|
|
916
|
-
|
|
917
|
-
```pseudocode
|
|
918
|
-
test "should fetch user data" async:
|
|
919
|
-
user = await fetchUser(123)
|
|
920
|
-
expect user.id equals 123
|
|
921
|
-
```
|
|
922
|
-
|
|
923
|
-
## Test Naming
|
|
924
|
-
|
|
925
|
-
Use clear, descriptive names:
|
|
926
|
-
|
|
927
|
-
```pseudocode
|
|
928
|
-
// ❌ Bad
|
|
929
|
-
test "test1"
|
|
930
|
-
test "it works"
|
|
931
|
-
|
|
932
|
-
// ✅ Good
|
|
933
|
-
test "should return user when ID exists"
|
|
934
|
-
test "should throw error when ID is invalid"
|
|
935
|
-
```
|
|
936
|
-
|
|
937
|
-
## Running Tests
|
|
938
|
-
|
|
939
|
-
```bash
|
|
940
|
-
# Run all tests
|
|
941
|
-
run-tests
|
|
942
|
-
|
|
943
|
-
# Run specific test file
|
|
944
|
-
run-tests user-test
|
|
945
|
-
|
|
946
|
-
# Watch mode (re-run on changes)
|
|
947
|
-
run-tests --watch
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
## Best Practices
|
|
951
|
-
|
|
952
|
-
1. **One test, one thing** - Test one behavior per test
|
|
953
|
-
2. **Independent tests** - Tests should not depend on each other
|
|
954
|
-
3. **Clear names** - Name should describe what is being tested
|
|
955
|
-
4. **Fast tests** - Tests should run quickly
|
|
956
|
-
|
|
957
|
-
```pseudocode
|
|
958
|
-
// ❌ Bad: Testing multiple things
|
|
959
|
-
test "user operations":
|
|
960
|
-
expect createUser("Bob").name equals "Bob"
|
|
961
|
-
expect deleteUser(1) equals true
|
|
962
|
-
expect listUsers().length equals 0
|
|
963
|
-
|
|
964
|
-
// ✅ Good: One test per operation
|
|
965
|
-
test "should create user with given name":
|
|
966
|
-
user = createUser("Bob")
|
|
967
|
-
expect user.name equals "Bob"
|
|
968
|
-
|
|
969
|
-
test "should delete user by ID":
|
|
970
|
-
result = deleteUser(1)
|
|
971
|
-
expect result equals true
|
|
972
|
-
|
|
973
|
-
test "should return empty list when no users":
|
|
974
|
-
users = listUsers()
|
|
975
|
-
expect users.length equals 0
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
## When to Write Tests
|
|
979
|
-
|
|
980
|
-
- **Before fixing bugs** - Write test that fails, then fix
|
|
981
|
-
- **For new features** - Test expected behavior
|
|
982
|
-
- **For edge cases** - Empty input, null values, large numbers
|
|
983
|
-
|
|
984
|
-
## What to Test
|
|
985
|
-
|
|
986
|
-
✅ **Do test:**
|
|
987
|
-
- Public functions and methods
|
|
988
|
-
- Edge cases (empty, null, zero, negative)
|
|
989
|
-
- Error conditions
|
|
990
|
-
|
|
991
|
-
❌ **Don't test:**
|
|
992
|
-
- Private implementation details
|
|
993
|
-
- Third-party libraries (they're already tested)
|
|
994
|
-
- Getters/setters with no logic
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
---
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
## Security
|
|
1001
|
-
|
|
1002
|
-
# Injection Prevention
|
|
1003
|
-
|
|
1004
|
-
## SQL Injection Prevention
|
|
1005
|
-
|
|
1006
|
-
```typescript
|
|
1007
|
-
// ❌ DANGEROUS: String concatenation
|
|
1008
|
-
const getUserByEmail = async (email: string) => {
|
|
1009
|
-
const query = `SELECT * FROM users WHERE email = '${email}'`;
|
|
1010
|
-
// Input: ' OR '1'='1
|
|
1011
|
-
// Result: SELECT * FROM users WHERE email = '' OR '1'='1'
|
|
1012
|
-
return db.query(query);
|
|
1013
|
-
};
|
|
1014
|
-
|
|
1015
|
-
// ✅ SAFE: Parameterized queries
|
|
1016
|
-
const getUserByEmail = async (email: string) => {
|
|
1017
|
-
return db.query('SELECT * FROM users WHERE email = ?', [email]);
|
|
1018
|
-
};
|
|
1019
|
-
|
|
1020
|
-
// ✅ SAFE: Using ORM
|
|
1021
|
-
const getUserByEmail = async (email: string) => {
|
|
1022
|
-
return userRepository.findOne({ where: { email } });
|
|
1023
|
-
};
|
|
1024
|
-
|
|
1025
|
-
// ✅ SAFE: Query builder
|
|
1026
|
-
const getUsers = async (minAge: number) => {
|
|
1027
|
-
return db
|
|
1028
|
-
.select('*')
|
|
1029
|
-
.from('users')
|
|
1030
|
-
.where('age', '>', minAge); // Automatically parameterized
|
|
1031
|
-
};
|
|
1032
|
-
```
|
|
1033
|
-
|
|
1034
|
-
## NoSQL Injection Prevention
|
|
1035
|
-
|
|
1036
|
-
```typescript
|
|
1037
|
-
// ❌ DANGEROUS: Accepting objects from user input
|
|
1038
|
-
app.post('/login', (req, res) => {
|
|
1039
|
-
const { username, password } = req.body;
|
|
1040
|
-
// If password = {$gt: ""}, it bypasses password check!
|
|
1041
|
-
db.users.findOne({ username, password });
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
|
-
// ✅ SAFE: Validate input types
|
|
1045
|
-
app.post('/login', (req, res) => {
|
|
1046
|
-
const { username, password } = req.body;
|
|
1047
|
-
|
|
1048
|
-
if (typeof username !== 'string' || typeof password !== 'string') {
|
|
1049
|
-
throw new Error('Invalid input types');
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
db.users.findOne({ username, password });
|
|
1053
|
-
});
|
|
1054
|
-
```
|
|
1055
|
-
|
|
1056
|
-
## Command Injection Prevention
|
|
1057
|
-
|
|
1058
|
-
```typescript
|
|
1059
|
-
// ❌ DANGEROUS: Shell command with user input
|
|
1060
|
-
const convertImage = async (filename: string) => {
|
|
1061
|
-
exec(`convert ${filename} output.jpg`);
|
|
1062
|
-
// Input: "file.png; rm -rf /"
|
|
1063
|
-
};
|
|
1064
|
-
|
|
1065
|
-
// ✅ SAFE: Use arrays, avoid shell
|
|
1066
|
-
import { execFile } from 'child_process';
|
|
1067
|
-
|
|
1068
|
-
const convertImage = async (filename: string) => {
|
|
1069
|
-
execFile('convert', [filename, 'output.jpg']);
|
|
1070
|
-
};
|
|
1071
|
-
|
|
1072
|
-
// ✅ SAFE: Validate input against whitelist
|
|
1073
|
-
const allowedFilename = /^[a-zA-Z0-9_-]+\.(png|jpg|gif)$/;
|
|
1074
|
-
if (!allowedFilename.test(filename)) {
|
|
1075
|
-
throw new Error('Invalid filename');
|
|
1076
|
-
}
|
|
1077
|
-
```
|
|
1078
|
-
|
|
1079
|
-
## Path Traversal Prevention
|
|
1080
|
-
|
|
1081
|
-
```typescript
|
|
1082
|
-
// ❌ DANGEROUS: Direct path usage
|
|
1083
|
-
app.get('/files/:filename', (req, res) => {
|
|
1084
|
-
res.sendFile(`/uploads/${req.params.filename}`);
|
|
1085
|
-
// Input: ../../etc/passwd
|
|
1086
|
-
});
|
|
1087
|
-
|
|
1088
|
-
// ✅ SAFE: Validate and normalize path
|
|
1089
|
-
import path from 'path';
|
|
1090
|
-
|
|
1091
|
-
app.get('/files/:filename', (req, res) => {
|
|
1092
|
-
const safeName = path.basename(req.params.filename);
|
|
1093
|
-
const filePath = path.join('/uploads', safeName);
|
|
1094
|
-
const normalizedPath = path.normalize(filePath);
|
|
1095
|
-
|
|
1096
|
-
if (!normalizedPath.startsWith('/uploads/')) {
|
|
1097
|
-
return res.status(400).json({ error: 'Invalid filename' });
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
res.sendFile(normalizedPath);
|
|
1101
|
-
});
|
|
1102
|
-
```
|
|
1103
|
-
|
|
1104
|
-
## Input Validation
|
|
1105
|
-
|
|
1106
|
-
```typescript
|
|
1107
|
-
// ✅ Whitelist validation
|
|
1108
|
-
import { z } from 'zod';
|
|
1109
|
-
|
|
1110
|
-
const userSchema = z.object({
|
|
1111
|
-
email: z.string().email(),
|
|
1112
|
-
password: z.string().min(12).max(160),
|
|
1113
|
-
age: z.number().int().min(0).max(150),
|
|
1114
|
-
role: z.enum(['user', 'admin'])
|
|
1115
|
-
});
|
|
1116
|
-
|
|
1117
|
-
const validateUser = (data: unknown) => {
|
|
1118
|
-
return userSchema.parse(data);
|
|
1119
|
-
};
|
|
1120
|
-
```
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
# Authentication & JWT Security
|
|
1124
|
-
|
|
1125
|
-
## Password Storage
|
|
1126
|
-
|
|
1127
|
-
```typescript
|
|
1128
|
-
import bcrypt from 'bcrypt';
|
|
1129
|
-
|
|
1130
|
-
const SALT_ROUNDS = 12; // Work factor
|
|
1131
|
-
|
|
1132
|
-
// ✅ Hash password with bcrypt
|
|
1133
|
-
async function hashPassword(password: string): Promise<string> {
|
|
1134
|
-
return bcrypt.hash(password, SALT_ROUNDS);
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
|
1138
|
-
return bcrypt.compare(password, hash);
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// ✅ Validate password strength
|
|
1142
|
-
function validatePassword(password: string): void {
|
|
1143
|
-
if (password.length < 12) {
|
|
1144
|
-
throw new Error('Password must be at least 12 characters');
|
|
1145
|
-
}
|
|
1146
|
-
if (password.length > 160) {
|
|
1147
|
-
throw new Error('Password too long'); // Prevent DoS via bcrypt
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
```
|
|
1151
|
-
|
|
1152
|
-
## JWT Best Practices
|
|
1153
|
-
|
|
1154
|
-
```typescript
|
|
1155
|
-
import jwt from 'jsonwebtoken';
|
|
1156
|
-
|
|
1157
|
-
const JWT_SECRET = process.env.JWT_SECRET!;
|
|
1158
|
-
const ACCESS_TOKEN_EXPIRY = '15m';
|
|
1159
|
-
const REFRESH_TOKEN_EXPIRY = '7d';
|
|
1160
|
-
|
|
1161
|
-
// ✅ Generate tokens
|
|
1162
|
-
function generateTokens(userId: string) {
|
|
1163
|
-
const accessToken = jwt.sign(
|
|
1164
|
-
{ sub: userId, type: 'access' },
|
|
1165
|
-
JWT_SECRET,
|
|
1166
|
-
{ expiresIn: ACCESS_TOKEN_EXPIRY }
|
|
1167
|
-
);
|
|
1168
|
-
|
|
1169
|
-
const refreshToken = jwt.sign(
|
|
1170
|
-
{ sub: userId, type: 'refresh' },
|
|
1171
|
-
JWT_SECRET,
|
|
1172
|
-
{ expiresIn: REFRESH_TOKEN_EXPIRY }
|
|
1173
|
-
);
|
|
1174
|
-
|
|
1175
|
-
return { accessToken, refreshToken };
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
// ✅ Verify and decode token
|
|
1179
|
-
function verifyToken(token: string) {
|
|
1180
|
-
try {
|
|
1181
|
-
return jwt.verify(token, JWT_SECRET);
|
|
1182
|
-
} catch (error) {
|
|
1183
|
-
if (error instanceof jwt.TokenExpiredError) {
|
|
1184
|
-
throw new UnauthorizedError('Token expired');
|
|
1185
|
-
}
|
|
1186
|
-
throw new UnauthorizedError('Invalid token');
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
```
|
|
1190
|
-
|
|
1191
|
-
## Login Protection
|
|
1192
|
-
|
|
1193
|
-
```typescript
|
|
1194
|
-
import rateLimit from 'express-rate-limit';
|
|
1195
|
-
|
|
1196
|
-
// ✅ Rate limit login attempts
|
|
1197
|
-
const loginLimiter = rateLimit({
|
|
1198
|
-
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
1199
|
-
max: 5, // 5 attempts
|
|
1200
|
-
message: 'Too many login attempts, please try again later',
|
|
1201
|
-
});
|
|
1202
|
-
|
|
1203
|
-
app.post('/login', loginLimiter, async (req, res) => {
|
|
1204
|
-
const { email, password } = req.body;
|
|
1205
|
-
|
|
1206
|
-
const user = await userService.findByEmail(email);
|
|
1207
|
-
|
|
1208
|
-
// ✅ Generic error message (don't reveal if user exists)
|
|
1209
|
-
if (!user || !await verifyPassword(password, user.passwordHash)) {
|
|
1210
|
-
return res.status(401).json({ error: 'Invalid email or password' });
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
const tokens = generateTokens(user.id);
|
|
1214
|
-
|
|
1215
|
-
// Regenerate session to prevent fixation
|
|
1216
|
-
req.session.regenerate(() => {
|
|
1217
|
-
res.json({ ...tokens });
|
|
1218
|
-
});
|
|
1219
|
-
});
|
|
1220
|
-
```
|
|
1221
|
-
|
|
1222
|
-
## Session Security
|
|
1223
|
-
|
|
1224
|
-
```typescript
|
|
1225
|
-
app.use(session({
|
|
1226
|
-
secret: process.env.SESSION_SECRET!,
|
|
1227
|
-
name: 'sessionId', // Don't use default 'connect.sid'
|
|
1228
|
-
|
|
1229
|
-
cookie: {
|
|
1230
|
-
secure: true, // HTTPS only
|
|
1231
|
-
httpOnly: true, // Prevent XSS access
|
|
1232
|
-
sameSite: 'strict', // CSRF protection
|
|
1233
|
-
maxAge: 30 * 60 * 1000, // 30 minutes
|
|
1234
|
-
},
|
|
1235
|
-
|
|
1236
|
-
resave: false,
|
|
1237
|
-
saveUninitialized: false,
|
|
1238
|
-
store: new RedisStore({ client: redisClient })
|
|
1239
|
-
}));
|
|
1240
|
-
|
|
1241
|
-
// ✅ Session regeneration after login
|
|
1242
|
-
app.post('/login', async (req, res, next) => {
|
|
1243
|
-
// ... authenticate user ...
|
|
1244
|
-
|
|
1245
|
-
req.session.regenerate((err) => {
|
|
1246
|
-
req.session.userId = user.id;
|
|
1247
|
-
res.json({ success: true });
|
|
1248
|
-
});
|
|
1249
|
-
});
|
|
1250
|
-
```
|
|
1251
|
-
|
|
1252
|
-
## Authorization Middleware
|
|
1253
|
-
|
|
1254
|
-
```typescript
|
|
1255
|
-
// ✅ Require authentication
|
|
1256
|
-
const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
|
|
1257
|
-
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
1258
|
-
|
|
1259
|
-
if (!token) {
|
|
1260
|
-
return res.status(401).json({ error: 'Authentication required' });
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
try {
|
|
1264
|
-
const payload = verifyToken(token);
|
|
1265
|
-
req.user = await userService.findById(payload.sub);
|
|
1266
|
-
next();
|
|
1267
|
-
} catch (error) {
|
|
1268
|
-
res.status(401).json({ error: 'Invalid token' });
|
|
1269
|
-
}
|
|
1270
|
-
};
|
|
1271
|
-
|
|
1272
|
-
// ✅ Require specific role
|
|
1273
|
-
const requireRole = (...roles: string[]) => {
|
|
1274
|
-
return (req: Request, res: Response, next: NextFunction) => {
|
|
1275
|
-
if (!roles.includes(req.user.role)) {
|
|
1276
|
-
return res.status(403).json({ error: 'Forbidden' });
|
|
1277
|
-
}
|
|
1278
|
-
next();
|
|
1279
|
-
};
|
|
1280
|
-
};
|
|
1281
|
-
```
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
# Secrets Management
|
|
1285
|
-
|
|
1286
|
-
## Environment Variables
|
|
1287
|
-
|
|
1288
|
-
```typescript
|
|
1289
|
-
// ❌ NEVER hardcode secrets
|
|
1290
|
-
const config = {
|
|
1291
|
-
dbPassword: 'super_secret_password',
|
|
1292
|
-
apiKey: 'sk-1234567890abcdef'
|
|
1293
|
-
};
|
|
1294
|
-
|
|
1295
|
-
// ✅ Use environment variables
|
|
1296
|
-
import dotenv from 'dotenv';
|
|
1297
|
-
dotenv.config();
|
|
1298
|
-
|
|
1299
|
-
const config = {
|
|
1300
|
-
dbPassword: process.env.DB_PASSWORD,
|
|
1301
|
-
apiKey: process.env.API_KEY,
|
|
1302
|
-
sessionSecret: process.env.SESSION_SECRET
|
|
1303
|
-
};
|
|
1304
|
-
```
|
|
1305
|
-
|
|
1306
|
-
## Validate Required Secrets
|
|
1307
|
-
|
|
1308
|
-
```typescript
|
|
1309
|
-
// ✅ Fail fast if secrets missing
|
|
1310
|
-
const requiredEnvVars = [
|
|
1311
|
-
'DB_PASSWORD',
|
|
1312
|
-
'API_KEY',
|
|
1313
|
-
'SESSION_SECRET',
|
|
1314
|
-
'JWT_SECRET'
|
|
1315
|
-
];
|
|
1316
|
-
|
|
1317
|
-
requiredEnvVars.forEach(varName => {
|
|
1318
|
-
if (!process.env[varName]) {
|
|
1319
|
-
throw new Error(`Missing required environment variable: ${varName}`);
|
|
1320
|
-
}
|
|
1321
|
-
});
|
|
1322
|
-
|
|
1323
|
-
// ✅ Type-safe config
|
|
1324
|
-
interface Config {
|
|
1325
|
-
dbPassword: string;
|
|
1326
|
-
apiKey: string;
|
|
1327
|
-
sessionSecret: string;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
function loadConfig(): Config {
|
|
1331
|
-
const dbPassword = process.env.DB_PASSWORD;
|
|
1332
|
-
if (!dbPassword) throw new Error('DB_PASSWORD required');
|
|
1333
|
-
|
|
1334
|
-
// ... validate all required vars
|
|
1335
|
-
|
|
1336
|
-
return { dbPassword, apiKey, sessionSecret };
|
|
1337
|
-
}
|
|
1338
|
-
```
|
|
1339
|
-
|
|
1340
|
-
## Generate Strong Secrets
|
|
1341
|
-
|
|
1342
|
-
```bash
|
|
1343
|
-
# Generate cryptographically secure secrets
|
|
1344
|
-
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
|
1345
|
-
|
|
1346
|
-
# Or using OpenSSL
|
|
1347
|
-
openssl rand -base64 32
|
|
1348
|
-
|
|
1349
|
-
# Or using head
|
|
1350
|
-
head -c32 /dev/urandom | base64
|
|
1351
|
-
```
|
|
1352
|
-
|
|
1353
|
-
## .gitignore Configuration
|
|
1354
|
-
|
|
1355
|
-
```bash
|
|
1356
|
-
# .gitignore - NEVER commit secrets
|
|
1357
|
-
.env
|
|
1358
|
-
.env.local
|
|
1359
|
-
.env.*.local
|
|
1360
|
-
*.key
|
|
1361
|
-
*.pem
|
|
1362
|
-
secrets/
|
|
1363
|
-
credentials.json
|
|
1364
|
-
```
|
|
1365
|
-
|
|
1366
|
-
## Environment Example File
|
|
1367
|
-
|
|
1368
|
-
```bash
|
|
1369
|
-
# .env.example - commit this to show required variables
|
|
1370
|
-
DB_HOST=localhost
|
|
1371
|
-
DB_PORT=5432
|
|
1372
|
-
DB_NAME=myapp
|
|
1373
|
-
DB_USER=
|
|
1374
|
-
DB_PASSWORD=
|
|
1375
|
-
|
|
1376
|
-
API_KEY=
|
|
1377
|
-
SESSION_SECRET=
|
|
1378
|
-
JWT_SECRET=
|
|
1379
|
-
|
|
1380
|
-
# Copy to .env and fill in actual values
|
|
1381
|
-
```
|
|
1382
|
-
|
|
1383
|
-
## Secrets in CI/CD
|
|
1384
|
-
|
|
1385
|
-
```yaml
|
|
1386
|
-
# GitHub Actions
|
|
1387
|
-
- name: Deploy
|
|
1388
|
-
env:
|
|
1389
|
-
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
|
|
1390
|
-
API_KEY: ${{ secrets.API_KEY }}
|
|
1391
|
-
run: ./deploy.sh
|
|
1392
|
-
|
|
1393
|
-
# ❌ Never echo secrets in logs
|
|
1394
|
-
- name: Configure
|
|
1395
|
-
run: |
|
|
1396
|
-
echo "Configuring application..."
|
|
1397
|
-
# echo "DB_PASSWORD=$DB_PASSWORD" # NEVER do this!
|
|
1398
|
-
```
|
|
1399
|
-
|
|
1400
|
-
## Secrets Rotation
|
|
1401
|
-
|
|
1402
|
-
```typescript
|
|
1403
|
-
// ✅ Support for rotating secrets
|
|
1404
|
-
class SecretManager {
|
|
1405
|
-
async getSecret(name: string): Promise<string> {
|
|
1406
|
-
// Check for new secret first (during rotation)
|
|
1407
|
-
const newSecret = process.env[`${name}_NEW`];
|
|
1408
|
-
if (newSecret) {
|
|
1409
|
-
return newSecret;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
const secret = process.env[name];
|
|
1413
|
-
if (!secret) {
|
|
1414
|
-
throw new Error(`Secret ${name} not found`);
|
|
1415
|
-
}
|
|
1416
|
-
return secret;
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
// ✅ Accept multiple JWT signing keys during rotation
|
|
1421
|
-
function verifyToken(token: string) {
|
|
1422
|
-
const keys = [process.env.JWT_SECRET, process.env.JWT_SECRET_OLD].filter(Boolean);
|
|
1423
|
-
|
|
1424
|
-
for (const key of keys) {
|
|
1425
|
-
try {
|
|
1426
|
-
return jwt.verify(token, key);
|
|
1427
|
-
} catch {}
|
|
1428
|
-
}
|
|
1429
|
-
throw new Error('Invalid token');
|
|
1430
|
-
}
|
|
1431
|
-
```
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
---
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
## Performance
|
|
1438
|
-
|
|
1439
|
-
# Performance Basics
|
|
1440
|
-
|
|
1441
|
-
## Choose the Right Data Structure
|
|
1442
|
-
|
|
1443
|
-
Different data structures have different speeds for different operations.
|
|
1444
|
-
|
|
1445
|
-
### Arrays vs Objects vs Maps
|
|
1446
|
-
|
|
1447
|
-
```pseudocode
|
|
1448
|
-
// ❌ Slow: Looking up in array (O(n))
|
|
1449
|
-
users = [
|
|
1450
|
-
{ id: 1, name: 'Alice' },
|
|
1451
|
-
{ id: 2, name: 'Bob' },
|
|
1452
|
-
// ... 1000 more
|
|
1453
|
-
]
|
|
1454
|
-
user = users.find(u => u.id == 500) // Checks 500 items
|
|
1455
|
-
|
|
1456
|
-
// ✅ Fast: Looking up in Map (O(1))
|
|
1457
|
-
users = Map()
|
|
1458
|
-
users.set(1, { id: 1, name: 'Alice' })
|
|
1459
|
-
users.set(2, { id: 2, name: 'Bob' })
|
|
1460
|
-
|
|
1461
|
-
user = users.get(500) // Instant lookup
|
|
1462
|
-
```
|
|
1463
|
-
|
|
1464
|
-
### Arrays vs Sets
|
|
1465
|
-
|
|
1466
|
-
```pseudocode
|
|
1467
|
-
// ❌ Slow: Checking if item exists in array
|
|
1468
|
-
items = [1, 2, 3, 4, 5, ... 1000 more]
|
|
1469
|
-
if items.contains(500): // Checks every item
|
|
1470
|
-
|
|
1471
|
-
// ✅ Fast: Checking if item exists in Set
|
|
1472
|
-
items = Set([1, 2, 3, 4, 5, ... 1000 more])
|
|
1473
|
-
if items.has(500): // Instant check
|
|
1474
|
-
```
|
|
1475
|
-
|
|
1476
|
-
### When to Use Each
|
|
1477
|
-
|
|
1478
|
-
| Data Structure | Good For |
|
|
1479
|
-
|----------------|----------|
|
|
1480
|
-
| **Array/List** | Ordered items, iteration |
|
|
1481
|
-
| **Object/Dict** | Key-value pairs (string keys) |
|
|
1482
|
-
| **Map** | Key-value pairs (any type keys), frequent lookups |
|
|
1483
|
-
| **Set** | Unique values, membership checks |
|
|
1484
|
-
|
|
1485
|
-
## Avoid N+1 Queries
|
|
1486
|
-
|
|
1487
|
-
One of the most common performance problems.
|
|
1488
|
-
|
|
1489
|
-
```pseudocode
|
|
1490
|
-
// ❌ BAD: N+1 queries (1 + N database calls)
|
|
1491
|
-
orders = database.query("SELECT * FROM orders")
|
|
1492
|
-
|
|
1493
|
-
for each order in orders:
|
|
1494
|
-
// Separate query for EACH order! 😱
|
|
1495
|
-
customer = database.query(
|
|
1496
|
-
"SELECT * FROM customers WHERE id = ?",
|
|
1497
|
-
[order.customer_id]
|
|
1498
|
-
)
|
|
1499
|
-
order.customer = customer
|
|
1500
|
-
// If 100 orders = 101 database calls!
|
|
1501
|
-
|
|
1502
|
-
// ✅ GOOD: Single query with JOIN
|
|
1503
|
-
ordersWithCustomers = database.query("
|
|
1504
|
-
SELECT
|
|
1505
|
-
orders.*,
|
|
1506
|
-
customers.name as customer_name,
|
|
1507
|
-
customers.email as customer_email
|
|
1508
|
-
FROM orders
|
|
1509
|
-
JOIN customers ON orders.customer_id = customers.id
|
|
1510
|
-
")
|
|
1511
|
-
// Only 1 database call!
|
|
1512
|
-
```
|
|
1513
|
-
|
|
1514
|
-
## Don't Load What You Don't Need
|
|
1515
|
-
|
|
1516
|
-
```pseudocode
|
|
1517
|
-
// ❌ Bad: Fetching entire object when you only need one field
|
|
1518
|
-
user = database.query("SELECT * FROM users WHERE id = ?", [id])
|
|
1519
|
-
print(user.email)
|
|
1520
|
-
|
|
1521
|
-
// ✅ Good: Fetch only what you need
|
|
1522
|
-
result = database.query(
|
|
1523
|
-
"SELECT email FROM users WHERE id = ?",
|
|
1524
|
-
[id]
|
|
1525
|
-
)
|
|
1526
|
-
print(result.email)
|
|
1527
|
-
|
|
1528
|
-
// ❌ Bad: Loading all records
|
|
1529
|
-
users = database.query("SELECT * FROM users")
|
|
1530
|
-
|
|
1531
|
-
// ✅ Good: Add LIMIT
|
|
1532
|
-
users = database.query("SELECT * FROM users LIMIT 100")
|
|
1533
|
-
```
|
|
1534
|
-
|
|
1535
|
-
## Use Async for I/O Operations
|
|
1536
|
-
|
|
1537
|
-
Don't block the program waiting for slow operations.
|
|
1538
|
-
|
|
1539
|
-
```pseudocode
|
|
1540
|
-
// ❌ Slow: Blocking operations (synchronous)
|
|
1541
|
-
file1 = readFileSync("file1.txt")
|
|
1542
|
-
file2 = readFileSync("file2.txt")
|
|
1543
|
-
file3 = readFileSync("file3.txt")
|
|
1544
|
-
// Total: 300ms (100ms each, one after another)
|
|
1545
|
-
|
|
1546
|
-
// ✅ Fast: Async operations (parallel)
|
|
1547
|
-
files = await Promise.all([
|
|
1548
|
-
readFile("file1.txt"),
|
|
1549
|
-
readFile("file2.txt"),
|
|
1550
|
-
readFile("file3.txt")
|
|
1551
|
-
])
|
|
1552
|
-
// Total: 100ms (all at once)
|
|
1553
|
-
```
|
|
1554
|
-
|
|
1555
|
-
## Avoid Unnecessary Work in Loops
|
|
1556
|
-
|
|
1557
|
-
```pseudocode
|
|
1558
|
-
// ❌ Bad: Work done every iteration
|
|
1559
|
-
for i in 0 to items.length:
|
|
1560
|
-
total = calculateTotal(items) // Recalculated each time!
|
|
1561
|
-
if items[i].price > total * 0.1:
|
|
1562
|
-
// ...
|
|
1563
|
-
|
|
1564
|
-
// ✅ Good: Work done once
|
|
1565
|
-
total = calculateTotal(items)
|
|
1566
|
-
for i in 0 to items.length:
|
|
1567
|
-
if items[i].price > total * 0.1:
|
|
1568
|
-
// ...
|
|
1569
|
-
|
|
1570
|
-
// ❌ Bad: Array length calculated each time
|
|
1571
|
-
for i in 0 to items.length:
|
|
1572
|
-
// ...
|
|
1573
|
-
|
|
1574
|
-
// ✅ Good: Length cached (minor improvement)
|
|
1575
|
-
len = items.length
|
|
1576
|
-
for i in 0 to len:
|
|
1577
|
-
// ...
|
|
1578
|
-
|
|
1579
|
-
// ✅ Best: Modern for-each loop
|
|
1580
|
-
for each item in items:
|
|
1581
|
-
// ...
|
|
1582
|
-
```
|
|
1583
|
-
|
|
1584
|
-
## Index Your Database
|
|
1585
|
-
|
|
1586
|
-
Indexes make lookups fast, but slow down writes.
|
|
1587
|
-
|
|
1588
|
-
```sql
|
|
1589
|
-
-- Without index: Checks every row
|
|
1590
|
-
SELECT * FROM users WHERE email = 'alice@example.com';
|
|
1591
|
-
-- With 1 million users: ~1 second
|
|
1592
|
-
|
|
1593
|
-
-- Add index
|
|
1594
|
-
CREATE INDEX idx_users_email ON users(email);
|
|
1595
|
-
|
|
1596
|
-
-- Now same query is instant
|
|
1597
|
-
SELECT * FROM users WHERE email = 'alice@example.com';
|
|
1598
|
-
-- With 1 million users: ~1 millisecond
|
|
1599
|
-
```
|
|
1600
|
-
|
|
1601
|
-
### When to Add Indexes
|
|
1602
|
-
|
|
1603
|
-
- Columns used in WHERE clauses
|
|
1604
|
-
- Columns used in JOIN conditions
|
|
1605
|
-
- Columns used in ORDER BY
|
|
1606
|
-
|
|
1607
|
-
```sql
|
|
1608
|
-
-- Frequently queried
|
|
1609
|
-
CREATE INDEX idx_orders_status ON orders(status);
|
|
1610
|
-
CREATE INDEX idx_users_email ON users(email);
|
|
1611
|
-
|
|
1612
|
-
-- Used in joins
|
|
1613
|
-
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
|
|
1614
|
-
```
|
|
1615
|
-
|
|
1616
|
-
## Cache Expensive Results
|
|
1617
|
-
|
|
1618
|
-
Don't recalculate the same thing repeatedly.
|
|
1619
|
-
|
|
1620
|
-
```pseudocode
|
|
1621
|
-
// ❌ Bad: Calculating every time
|
|
1622
|
-
function getReport(userId):
|
|
1623
|
-
data = expensiveCalculation(userId) // 5 seconds
|
|
1624
|
-
return data
|
|
1625
|
-
|
|
1626
|
-
// Called 100 times = 500 seconds!
|
|
1627
|
-
|
|
1628
|
-
// ✅ Good: Cache results
|
|
1629
|
-
cache = Map()
|
|
1630
|
-
|
|
1631
|
-
function getReport(userId):
|
|
1632
|
-
if cache.has(userId):
|
|
1633
|
-
return cache.get(userId) // Instant
|
|
1634
|
-
|
|
1635
|
-
data = expensiveCalculation(userId)
|
|
1636
|
-
cache.set(userId, data)
|
|
1637
|
-
return data
|
|
1638
|
-
|
|
1639
|
-
// First call: 5 seconds, next 99 calls: instant
|
|
1640
|
-
```
|
|
1641
|
-
|
|
1642
|
-
## Batch Operations
|
|
1643
|
-
|
|
1644
|
-
Process multiple items together instead of one at a time.
|
|
1645
|
-
|
|
1646
|
-
```pseudocode
|
|
1647
|
-
// ❌ Bad: Individual database calls
|
|
1648
|
-
for each user in users:
|
|
1649
|
-
database.execute("INSERT INTO users (name) VALUES (?)", [user.name])
|
|
1650
|
-
// 100 users = 100 database calls
|
|
1651
|
-
|
|
1652
|
-
// ✅ Good: Batch insert
|
|
1653
|
-
database.execute("
|
|
1654
|
-
INSERT INTO users (name)
|
|
1655
|
-
VALUES " + users.map(u => "(?)").join(", "),
|
|
1656
|
-
users.map(u => u.name)
|
|
1657
|
-
)
|
|
1658
|
-
// 100 users = 1 database call
|
|
1659
|
-
```
|
|
1660
|
-
|
|
1661
|
-
## Profile Before Optimizing
|
|
1662
|
-
|
|
1663
|
-
Don't guess where the problem is - measure!
|
|
1664
|
-
|
|
1665
|
-
```pseudocode
|
|
1666
|
-
// Measure execution time
|
|
1667
|
-
startTime = currentTime()
|
|
1668
|
-
result = await slowOperation()
|
|
1669
|
-
endTime = currentTime()
|
|
1670
|
-
print("Operation took:", endTime - startTime, "ms")
|
|
1671
|
-
|
|
1672
|
-
// Measure specific parts
|
|
1673
|
-
startDB = currentTime()
|
|
1674
|
-
data = await database.query("...")
|
|
1675
|
-
print("Database:", currentTime() - startDB, "ms")
|
|
1676
|
-
|
|
1677
|
-
startProcess = currentTime()
|
|
1678
|
-
processed = processData(data)
|
|
1679
|
-
print("Processing:", currentTime() - startProcess, "ms")
|
|
1680
|
-
```
|
|
1681
|
-
|
|
1682
|
-
## Common Performance Mistakes
|
|
1683
|
-
|
|
1684
|
-
### Mistake 1: Nested Loops with Database Queries
|
|
1685
|
-
|
|
1686
|
-
```pseudocode
|
|
1687
|
-
// ❌ TERRIBLE: Nested queries
|
|
1688
|
-
for each user in users:
|
|
1689
|
-
for each order in orders:
|
|
1690
|
-
product = await database.query(
|
|
1691
|
-
"SELECT * FROM products WHERE id = ?",
|
|
1692
|
-
[order.product_id]
|
|
1693
|
-
)
|
|
1694
|
-
// 100 users × 50 orders = 5000 database calls!
|
|
1695
|
-
|
|
1696
|
-
// ✅ GOOD: Load all data first
|
|
1697
|
-
products = await database.query("SELECT * FROM products")
|
|
1698
|
-
productMap = Map()
|
|
1699
|
-
for each p in products:
|
|
1700
|
-
productMap.set(p.id, p)
|
|
1701
|
-
|
|
1702
|
-
for each user in users:
|
|
1703
|
-
for each order in orders:
|
|
1704
|
-
product = productMap.get(order.product_id) // Instant
|
|
1705
|
-
```
|
|
1706
|
-
|
|
1707
|
-
### Mistake 2: Loading Everything into Memory
|
|
1708
|
-
|
|
1709
|
-
```pseudocode
|
|
1710
|
-
// ❌ Bad: Loading 1 million records
|
|
1711
|
-
allUsers = await database.query("SELECT * FROM users")
|
|
1712
|
-
// Crashes with out of memory!
|
|
1713
|
-
|
|
1714
|
-
// ✅ Good: Process in batches
|
|
1715
|
-
BATCH_SIZE = 1000
|
|
1716
|
-
offset = 0
|
|
1717
|
-
|
|
1718
|
-
while true:
|
|
1719
|
-
users = await database.query(
|
|
1720
|
-
"SELECT * FROM users LIMIT ? OFFSET ?",
|
|
1721
|
-
[BATCH_SIZE, offset]
|
|
1722
|
-
)
|
|
1723
|
-
|
|
1724
|
-
if users.length == 0:
|
|
1725
|
-
break
|
|
1726
|
-
|
|
1727
|
-
await processUsers(users)
|
|
1728
|
-
offset = offset + BATCH_SIZE
|
|
1729
|
-
```
|
|
1730
|
-
|
|
1731
|
-
### Mistake 3: Not Using Indexes
|
|
1732
|
-
|
|
1733
|
-
```sql
|
|
1734
|
-
-- ❌ Slow: No index on email column
|
|
1735
|
-
SELECT * FROM users WHERE email = 'alice@example.com';
|
|
1736
|
-
-- 1 million rows: ~2 seconds
|
|
1737
|
-
|
|
1738
|
-
-- ✅ Fast: Add index
|
|
1739
|
-
CREATE INDEX idx_users_email ON users(email);
|
|
1740
|
-
-- Same query: ~2 milliseconds
|
|
1741
|
-
```
|
|
1742
|
-
|
|
1743
|
-
## Quick Performance Checklist
|
|
1744
|
-
|
|
1745
|
-
- [ ] Use Map/Set for lookups instead of Array
|
|
1746
|
-
- [ ] Avoid N+1 queries (use JOINs)
|
|
1747
|
-
- [ ] Add database indexes on frequently queried columns
|
|
1748
|
-
- [ ] Don't load all records (use LIMIT)
|
|
1749
|
-
- [ ] Cache expensive calculations
|
|
1750
|
-
- [ ] Run independent operations in parallel
|
|
1751
|
-
- [ ] Move work outside of loops
|
|
1752
|
-
- [ ] Batch database operations
|
|
1753
|
-
- [ ] Profile before optimizing
|
|
1754
|
-
|
|
1755
|
-
## When to Optimize
|
|
1756
|
-
|
|
1757
|
-
1. **Measure first** - Is there actually a problem?
|
|
1758
|
-
2. **Find the bottleneck** - What's slow?
|
|
1759
|
-
3. **Fix the biggest problem** - Don't waste time on small gains
|
|
1760
|
-
4. **Measure again** - Did it help?
|
|
1761
|
-
|
|
1762
|
-
Don't optimize prematurely - write clear code first, optimize when needed!
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
# Async Performance Patterns
|
|
1766
|
-
|
|
1767
|
-
## Parallel Execution
|
|
1768
|
-
|
|
1769
|
-
```typescript
|
|
1770
|
-
// ❌ Sequential - slow
|
|
1771
|
-
async function getUserData(userId: string) {
|
|
1772
|
-
const user = await fetchUser(userId); // 100ms
|
|
1773
|
-
const posts = await fetchPosts(userId); // 150ms
|
|
1774
|
-
const comments = await fetchComments(userId); // 120ms
|
|
1775
|
-
return { user, posts, comments }; // Total: 370ms
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
// ✅ Parallel - fast
|
|
1779
|
-
async function getUserData(userId: string) {
|
|
1780
|
-
const [user, posts, comments] = await Promise.all([
|
|
1781
|
-
fetchUser(userId),
|
|
1782
|
-
fetchPosts(userId),
|
|
1783
|
-
fetchComments(userId)
|
|
1784
|
-
]);
|
|
1785
|
-
return { user, posts, comments }; // Total: 150ms
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
// ✅ Partial parallel with dependencies
|
|
1789
|
-
async function getOrderDetails(orderId: string) {
|
|
1790
|
-
const order = await fetchOrder(orderId); // Must fetch first
|
|
1791
|
-
|
|
1792
|
-
const [customer, items, shipping] = await Promise.all([
|
|
1793
|
-
fetchCustomer(order.customerId),
|
|
1794
|
-
fetchOrderItems(orderId),
|
|
1795
|
-
fetchShippingInfo(orderId)
|
|
1796
|
-
]);
|
|
1797
|
-
|
|
1798
|
-
return { order, customer, items, shipping };
|
|
1799
|
-
}
|
|
1800
|
-
```
|
|
1801
|
-
|
|
1802
|
-
## Promise.allSettled for Partial Failures
|
|
1803
|
-
|
|
1804
|
-
```typescript
|
|
1805
|
-
// Return partial data instead of complete failure
|
|
1806
|
-
async function getDashboard(userId: string) {
|
|
1807
|
-
const [user, orders, stats] = await Promise.allSettled([
|
|
1808
|
-
getUser(userId),
|
|
1809
|
-
getOrders(userId),
|
|
1810
|
-
getStats(userId)
|
|
1811
|
-
]);
|
|
1812
|
-
|
|
1813
|
-
return {
|
|
1814
|
-
user: user.status === 'fulfilled' ? user.value : null,
|
|
1815
|
-
orders: orders.status === 'fulfilled' ? orders.value : [],
|
|
1816
|
-
stats: stats.status === 'fulfilled' ? stats.value : null,
|
|
1817
|
-
errors: {
|
|
1818
|
-
user: user.status === 'rejected' ? user.reason.message : null,
|
|
1819
|
-
orders: orders.status === 'rejected' ? orders.reason.message : null,
|
|
1820
|
-
stats: stats.status === 'rejected' ? stats.reason.message : null
|
|
1821
|
-
}
|
|
1822
|
-
};
|
|
1823
|
-
}
|
|
1824
|
-
```
|
|
1825
|
-
|
|
1826
|
-
## Batch Processing
|
|
1827
|
-
|
|
1828
|
-
```typescript
|
|
1829
|
-
// ❌ One at a time - slow
|
|
1830
|
-
async function processUsers(userIds: string[]) {
|
|
1831
|
-
for (const id of userIds) {
|
|
1832
|
-
await updateUser(id);
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
// ✅ Batch processing
|
|
1837
|
-
async function processUsers(userIds: string[]) {
|
|
1838
|
-
const BATCH_SIZE = 50;
|
|
1839
|
-
|
|
1840
|
-
for (let i = 0; i < userIds.length; i += BATCH_SIZE) {
|
|
1841
|
-
const batch = userIds.slice(i, i + BATCH_SIZE);
|
|
1842
|
-
await Promise.all(batch.map(id => updateUser(id)));
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
// ✅ Bulk database operations
|
|
1847
|
-
async function createUsers(users: User[]) {
|
|
1848
|
-
await db.query(`
|
|
1849
|
-
INSERT INTO users (name, email)
|
|
1850
|
-
VALUES ${users.map(() => '(?, ?)').join(', ')}
|
|
1851
|
-
`, users.flatMap(u => [u.name, u.email]));
|
|
1852
|
-
}
|
|
1853
|
-
```
|
|
1854
|
-
|
|
1855
|
-
## Debouncing and Throttling
|
|
1856
|
-
|
|
1857
|
-
```typescript
|
|
1858
|
-
// Debounce: Wait until user stops typing
|
|
1859
|
-
const debounce = <T extends (...args: any[]) => any>(
|
|
1860
|
-
fn: T,
|
|
1861
|
-
delay: number
|
|
1862
|
-
): ((...args: Parameters<T>) => void) => {
|
|
1863
|
-
let timeoutId: NodeJS.Timeout;
|
|
1864
|
-
|
|
1865
|
-
return (...args: Parameters<T>) => {
|
|
1866
|
-
clearTimeout(timeoutId);
|
|
1867
|
-
timeoutId = setTimeout(() => fn(...args), delay);
|
|
1868
|
-
};
|
|
1869
|
-
};
|
|
1870
|
-
|
|
1871
|
-
// Throttle: Execute at most once per interval
|
|
1872
|
-
const throttle = <T extends (...args: any[]) => any>(
|
|
1873
|
-
fn: T,
|
|
1874
|
-
limit: number
|
|
1875
|
-
): ((...args: Parameters<T>) => void) => {
|
|
1876
|
-
let inThrottle: boolean;
|
|
1877
|
-
|
|
1878
|
-
return (...args: Parameters<T>) => {
|
|
1879
|
-
if (!inThrottle) {
|
|
1880
|
-
fn(...args);
|
|
1881
|
-
inThrottle = true;
|
|
1882
|
-
setTimeout(() => (inThrottle = false), limit);
|
|
1883
|
-
}
|
|
1884
|
-
};
|
|
1885
|
-
};
|
|
1886
|
-
|
|
1887
|
-
// Usage
|
|
1888
|
-
const searchUsers = debounce(query => api.search(query), 300);
|
|
1889
|
-
const handleScroll = throttle(() => console.log('scroll'), 100);
|
|
1890
|
-
```
|
|
1891
|
-
|
|
1892
|
-
## Rate Limiting Concurrent Operations
|
|
1893
|
-
|
|
1894
|
-
```typescript
|
|
1895
|
-
async function processWithLimit<T>(
|
|
1896
|
-
items: T[],
|
|
1897
|
-
fn: (item: T) => Promise<void>,
|
|
1898
|
-
concurrency: number
|
|
1899
|
-
): Promise<void> {
|
|
1900
|
-
const chunks = [];
|
|
1901
|
-
for (let i = 0; i < items.length; i += concurrency) {
|
|
1902
|
-
chunks.push(items.slice(i, i + concurrency));
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
for (const chunk of chunks) {
|
|
1906
|
-
await Promise.all(chunk.map(fn));
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
// Usage: Process 100 items, max 10 at a time
|
|
1911
|
-
await processWithLimit(users, updateUser, 10);
|
|
1912
|
-
```
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
---
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
## API Design
|
|
1919
|
-
|
|
1920
|
-
# API Basics
|
|
1921
|
-
|
|
1922
|
-
## HTTP Methods
|
|
1923
|
-
|
|
1924
|
-
Use the right method for each operation:
|
|
1925
|
-
|
|
1926
|
-
| Method | Purpose | Example |
|
|
1927
|
-
|--------|---------|---------|
|
|
1928
|
-
| GET | Read data | Get list of users |
|
|
1929
|
-
| POST | Create new resource | Create a new user |
|
|
1930
|
-
| PUT | Replace entire resource | Update all user fields |
|
|
1931
|
-
| PATCH | Update part of resource | Update user's email only |
|
|
1932
|
-
| DELETE | Remove resource | Delete a user |
|
|
1933
|
-
|
|
1934
|
-
## GET - Reading Data
|
|
1935
|
-
|
|
1936
|
-
```pseudocode
|
|
1937
|
-
// Get all items
|
|
1938
|
-
route GET "/api/users":
|
|
1939
|
-
users = getAllUsers()
|
|
1940
|
-
return JSON(users)
|
|
1941
|
-
|
|
1942
|
-
// Get single item by ID
|
|
1943
|
-
route GET "/api/users/:id":
|
|
1944
|
-
user = getUserById(params.id)
|
|
1945
|
-
if user is null:
|
|
1946
|
-
return status 404, JSON({ error: "User not found" })
|
|
1947
|
-
return JSON(user)
|
|
1948
|
-
```
|
|
1949
|
-
|
|
1950
|
-
## POST - Creating Data
|
|
1951
|
-
|
|
1952
|
-
```pseudocode
|
|
1953
|
-
route POST "/api/users":
|
|
1954
|
-
name = request.body.name
|
|
1955
|
-
email = request.body.email
|
|
1956
|
-
|
|
1957
|
-
// Validate input
|
|
1958
|
-
if name is empty or email is empty:
|
|
1959
|
-
return status 400, JSON({ error: "Name and email required" })
|
|
1960
|
-
|
|
1961
|
-
newUser = createUser({ name, email })
|
|
1962
|
-
|
|
1963
|
-
// Return 201 Created with new resource
|
|
1964
|
-
return status 201, JSON(newUser)
|
|
1965
|
-
```
|
|
1966
|
-
|
|
1967
|
-
## PUT - Replacing Data
|
|
1968
|
-
|
|
1969
|
-
```pseudocode
|
|
1970
|
-
route PUT "/api/users/:id":
|
|
1971
|
-
id = params.id
|
|
1972
|
-
name = request.body.name
|
|
1973
|
-
email = request.body.email
|
|
1974
|
-
|
|
1975
|
-
user = getUserById(id)
|
|
1976
|
-
if user is null:
|
|
1977
|
-
return status 404, JSON({ error: "User not found" })
|
|
1978
|
-
|
|
1979
|
-
updated = replaceUser(id, { name, email })
|
|
1980
|
-
return JSON(updated)
|
|
1981
|
-
```
|
|
1982
|
-
|
|
1983
|
-
## PATCH - Updating Data
|
|
1984
|
-
|
|
1985
|
-
```pseudocode
|
|
1986
|
-
route PATCH "/api/users/:id":
|
|
1987
|
-
id = params.id
|
|
1988
|
-
updates = request.body // Only fields to update
|
|
1989
|
-
|
|
1990
|
-
user = getUserById(id)
|
|
1991
|
-
if user is null:
|
|
1992
|
-
return status 404, JSON({ error: "User not found" })
|
|
1993
|
-
|
|
1994
|
-
updated = updateUser(id, updates)
|
|
1995
|
-
return JSON(updated)
|
|
1996
|
-
```
|
|
1997
|
-
|
|
1998
|
-
## DELETE - Removing Data
|
|
1999
|
-
|
|
2000
|
-
```pseudocode
|
|
2001
|
-
route DELETE "/api/users/:id":
|
|
2002
|
-
id = params.id
|
|
2003
|
-
|
|
2004
|
-
user = getUserById(id)
|
|
2005
|
-
if user is null:
|
|
2006
|
-
return status 404, JSON({ error: "User not found" })
|
|
2007
|
-
|
|
2008
|
-
deleteUser(id)
|
|
2009
|
-
|
|
2010
|
-
// 204 No Content - successful deletion
|
|
2011
|
-
return status 204
|
|
2012
|
-
```
|
|
2013
|
-
|
|
2014
|
-
## HTTP Status Codes
|
|
2015
|
-
|
|
2016
|
-
### Success Codes (2xx)
|
|
2017
|
-
|
|
2018
|
-
```pseudocode
|
|
2019
|
-
// 200 OK - Request succeeded
|
|
2020
|
-
return status 200, JSON(data)
|
|
2021
|
-
|
|
2022
|
-
// 201 Created - New resource created
|
|
2023
|
-
return status 201, JSON(newResource)
|
|
2024
|
-
|
|
2025
|
-
// 204 No Content - Success with no response body
|
|
2026
|
-
return status 204
|
|
2027
|
-
```
|
|
2028
|
-
|
|
2029
|
-
### Client Error Codes (4xx)
|
|
2030
|
-
|
|
2031
|
-
```pseudocode
|
|
2032
|
-
// 400 Bad Request - Invalid input
|
|
2033
|
-
return status 400, JSON({ error: "Invalid email format" })
|
|
2034
|
-
|
|
2035
|
-
// 401 Unauthorized - Not authenticated
|
|
2036
|
-
return status 401, JSON({ error: "Login required" })
|
|
2037
|
-
|
|
2038
|
-
// 403 Forbidden - Authenticated but not allowed
|
|
2039
|
-
return status 403, JSON({ error: "Admin access required" })
|
|
2040
|
-
|
|
2041
|
-
// 404 Not Found - Resource doesn't exist
|
|
2042
|
-
return status 404, JSON({ error: "User not found" })
|
|
2043
|
-
|
|
2044
|
-
// 409 Conflict - Resource already exists
|
|
2045
|
-
return status 409, JSON({ error: "Email already registered" })
|
|
2046
|
-
```
|
|
2047
|
-
|
|
2048
|
-
### Server Error Codes (5xx)
|
|
2049
|
-
|
|
2050
|
-
```pseudocode
|
|
2051
|
-
// 500 Internal Server Error - Unexpected error
|
|
2052
|
-
return status 500, JSON({ error: "Internal server error" })
|
|
2053
|
-
|
|
2054
|
-
// 503 Service Unavailable - Temporary issue
|
|
2055
|
-
return status 503, JSON({ error: "Database unavailable" })
|
|
2056
|
-
```
|
|
2057
|
-
|
|
2058
|
-
## URL Structure
|
|
2059
|
-
|
|
2060
|
-
Use clear, hierarchical URLs:
|
|
2061
|
-
|
|
2062
|
-
```
|
|
2063
|
-
✅ Good
|
|
2064
|
-
GET /api/users # List all users
|
|
2065
|
-
GET /api/users/123 # Get user 123
|
|
2066
|
-
POST /api/users # Create user
|
|
2067
|
-
GET /api/users/123/posts # Get posts by user 123
|
|
2068
|
-
|
|
2069
|
-
❌ Bad
|
|
2070
|
-
GET /api/getUsers
|
|
2071
|
-
POST /api/createUser
|
|
2072
|
-
GET /api/user?id=123
|
|
2073
|
-
```
|
|
2074
|
-
|
|
2075
|
-
## Request and Response Format
|
|
2076
|
-
|
|
2077
|
-
### JSON Request Body
|
|
2078
|
-
|
|
2079
|
-
```
|
|
2080
|
-
// Client sends
|
|
2081
|
-
POST /api/users
|
|
2082
|
-
Content-Type: application/json
|
|
2083
|
-
|
|
2084
|
-
{
|
|
2085
|
-
"name": "Alice",
|
|
2086
|
-
"email": "alice@example.com"
|
|
2087
|
-
}
|
|
2088
|
-
```
|
|
2089
|
-
|
|
2090
|
-
### JSON Response
|
|
2091
|
-
|
|
2092
|
-
```
|
|
2093
|
-
// Server responds
|
|
2094
|
-
HTTP/1.1 201 Created
|
|
2095
|
-
Content-Type: application/json
|
|
2096
|
-
|
|
2097
|
-
{
|
|
2098
|
-
"id": 123,
|
|
2099
|
-
"name": "Alice",
|
|
2100
|
-
"email": "alice@example.com",
|
|
2101
|
-
"createdAt": "2024-01-15T10:30:00Z"
|
|
2102
|
-
}
|
|
2103
|
-
```
|
|
2104
|
-
|
|
2105
|
-
## Query Parameters
|
|
2106
|
-
|
|
2107
|
-
Use query parameters for filtering, sorting, and pagination:
|
|
2108
|
-
|
|
2109
|
-
```pseudocode
|
|
2110
|
-
// Filter by status
|
|
2111
|
-
// GET /api/orders?status=pending
|
|
2112
|
-
route GET "/api/orders":
|
|
2113
|
-
status = query.status
|
|
2114
|
-
orders = getOrders({ status })
|
|
2115
|
-
return JSON(orders)
|
|
2116
|
-
|
|
2117
|
-
// Sort by field
|
|
2118
|
-
// GET /api/users?sort=name
|
|
2119
|
-
|
|
2120
|
-
// Pagination
|
|
2121
|
-
// GET /api/users?page=2&limit=20
|
|
2122
|
-
```
|
|
2123
|
-
|
|
2124
|
-
## Error Responses
|
|
2125
|
-
|
|
2126
|
-
Always return consistent error format:
|
|
2127
|
-
|
|
2128
|
-
```pseudocode
|
|
2129
|
-
// ✅ Good: Structured error
|
|
2130
|
-
return status 400, JSON({
|
|
2131
|
-
error: {
|
|
2132
|
-
code: "VALIDATION_ERROR",
|
|
2133
|
-
message: "Invalid input",
|
|
2134
|
-
details: {
|
|
2135
|
-
email: "Email format is invalid"
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
})
|
|
2139
|
-
|
|
2140
|
-
// ❌ Bad: Inconsistent
|
|
2141
|
-
return status 400, "Bad request"
|
|
2142
|
-
return status 400, JSON({ msg: "Error" })
|
|
2143
|
-
```
|
|
2144
|
-
|
|
2145
|
-
## Best Practices
|
|
2146
|
-
|
|
2147
|
-
1. **Use correct HTTP methods** - GET for reading, POST for creating, etc.
|
|
2148
|
-
2. **Use appropriate status codes** - 200 for success, 404 for not found, etc.
|
|
2149
|
-
3. **Return JSON** - Standard format for APIs
|
|
2150
|
-
4. **Validate input** - Check data before processing
|
|
2151
|
-
5. **Handle errors** - Return clear error messages
|
|
2152
|
-
|
|
2153
|
-
```pseudocode
|
|
2154
|
-
// Complete example
|
|
2155
|
-
route POST "/api/products":
|
|
2156
|
-
name = request.body.name
|
|
2157
|
-
price = request.body.price
|
|
2158
|
-
|
|
2159
|
-
// Validate
|
|
2160
|
-
if name is empty or price is empty:
|
|
2161
|
-
return status 400, JSON({
|
|
2162
|
-
error: "Name and price are required"
|
|
2163
|
-
})
|
|
2164
|
-
|
|
2165
|
-
if price < 0:
|
|
2166
|
-
return status 400, JSON({
|
|
2167
|
-
error: "Price cannot be negative"
|
|
2168
|
-
})
|
|
2169
|
-
|
|
2170
|
-
// Check for duplicates
|
|
2171
|
-
if productExists(name):
|
|
2172
|
-
return status 409, JSON({
|
|
2173
|
-
error: "Product already exists"
|
|
2174
|
-
})
|
|
2175
|
-
|
|
2176
|
-
// Create
|
|
2177
|
-
product = createProduct({ name, price })
|
|
2178
|
-
|
|
2179
|
-
// Return success
|
|
2180
|
-
return status 201, JSON(product)
|
|
2181
|
-
```
|
|
2182
|
-
|
|
2183
|
-
## Common Mistakes
|
|
2184
|
-
|
|
2185
|
-
```pseudocode
|
|
2186
|
-
// ❌ Wrong method for operation
|
|
2187
|
-
route GET "/api/users/delete/:id" // Should be DELETE
|
|
2188
|
-
|
|
2189
|
-
// ❌ Wrong status code
|
|
2190
|
-
route POST "/api/users":
|
|
2191
|
-
user = createUser(body)
|
|
2192
|
-
return status 200, JSON(user) // Should be 201
|
|
2193
|
-
|
|
2194
|
-
// ❌ Not handling missing resources
|
|
2195
|
-
route GET "/api/users/:id":
|
|
2196
|
-
user = getUserById(params.id)
|
|
2197
|
-
return JSON(user) // What if user is null?
|
|
2198
|
-
|
|
2199
|
-
// ✅ Correct
|
|
2200
|
-
route DELETE "/api/users/:id"
|
|
2201
|
-
|
|
2202
|
-
route POST "/api/users":
|
|
2203
|
-
user = createUser(body)
|
|
2204
|
-
return status 201, JSON(user)
|
|
2205
|
-
|
|
2206
|
-
route GET "/api/users/:id":
|
|
2207
|
-
user = getUserById(params.id)
|
|
2208
|
-
if user is null:
|
|
2209
|
-
return status 404, JSON({ error: "User not found" })
|
|
2210
|
-
return JSON(user)
|
|
2211
|
-
```
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
# REST API Design
|
|
2215
|
-
|
|
2216
|
-
## Resource-Oriented URLs
|
|
2217
|
-
|
|
2218
|
-
```
|
|
2219
|
-
✅ Good (nouns, plural)
|
|
2220
|
-
GET /api/v1/books # List books
|
|
2221
|
-
GET /api/v1/books/123 # Get book
|
|
2222
|
-
POST /api/v1/books # Create book
|
|
2223
|
-
PUT /api/v1/books/123 # Replace book
|
|
2224
|
-
PATCH /api/v1/books/123 # Update book
|
|
2225
|
-
DELETE /api/v1/books/123 # Delete book
|
|
2226
|
-
|
|
2227
|
-
❌ Bad (verbs, actions)
|
|
2228
|
-
POST /api/v1/createBook
|
|
2229
|
-
GET /api/v1/getBookById/123
|
|
2230
|
-
POST /api/v1/updateBook/123
|
|
2231
|
-
```
|
|
2232
|
-
|
|
2233
|
-
## HTTP Methods
|
|
2234
|
-
|
|
2235
|
-
```typescript
|
|
2236
|
-
// GET - Read (safe, idempotent)
|
|
2237
|
-
app.get('/api/v1/users/:id', async (req, res) => {
|
|
2238
|
-
const user = await userService.findById(req.params.id);
|
|
2239
|
-
res.json({ data: user });
|
|
2240
|
-
});
|
|
2241
|
-
|
|
2242
|
-
// POST - Create (not idempotent)
|
|
2243
|
-
app.post('/api/v1/users', async (req, res) => {
|
|
2244
|
-
const user = await userService.create(req.body);
|
|
2245
|
-
res.status(201)
|
|
2246
|
-
.location(`/api/v1/users/${user.id}`)
|
|
2247
|
-
.json({ data: user });
|
|
2248
|
-
});
|
|
2249
|
-
|
|
2250
|
-
// PUT - Replace entire resource (idempotent)
|
|
2251
|
-
app.put('/api/v1/users/:id', async (req, res) => {
|
|
2252
|
-
const user = await userService.replace(req.params.id, req.body);
|
|
2253
|
-
res.json({ data: user });
|
|
2254
|
-
});
|
|
2255
|
-
|
|
2256
|
-
// PATCH - Partial update (idempotent)
|
|
2257
|
-
app.patch('/api/v1/users/:id', async (req, res) => {
|
|
2258
|
-
const user = await userService.update(req.params.id, req.body);
|
|
2259
|
-
res.json({ data: user });
|
|
2260
|
-
});
|
|
2261
|
-
|
|
2262
|
-
// DELETE - Remove (idempotent)
|
|
2263
|
-
app.delete('/api/v1/users/:id', async (req, res) => {
|
|
2264
|
-
await userService.delete(req.params.id);
|
|
2265
|
-
res.status(204).end();
|
|
2266
|
-
});
|
|
2267
|
-
```
|
|
2268
|
-
|
|
2269
|
-
## Status Codes
|
|
2270
|
-
|
|
2271
|
-
```typescript
|
|
2272
|
-
// Success
|
|
2273
|
-
200 OK // GET, PUT, PATCH succeeded
|
|
2274
|
-
201 Created // POST succeeded
|
|
2275
|
-
204 No Content // DELETE succeeded
|
|
2276
|
-
|
|
2277
|
-
// Client errors
|
|
2278
|
-
400 Bad Request // Validation failed
|
|
2279
|
-
401 Unauthorized // Not authenticated
|
|
2280
|
-
403 Forbidden // Authenticated but not allowed
|
|
2281
|
-
404 Not Found // Resource doesn't exist
|
|
2282
|
-
409 Conflict // Duplicate, version conflict
|
|
2283
|
-
422 Unprocessable // Business rule violation
|
|
2284
|
-
|
|
2285
|
-
// Server errors
|
|
2286
|
-
500 Internal Server Error
|
|
2287
|
-
```
|
|
2288
|
-
|
|
2289
|
-
## Response Format
|
|
2290
|
-
|
|
2291
|
-
```typescript
|
|
2292
|
-
// Single resource
|
|
2293
|
-
{
|
|
2294
|
-
"data": {
|
|
2295
|
-
"id": 123,
|
|
2296
|
-
"name": "John Doe",
|
|
2297
|
-
"email": "john@example.com"
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
// Collection with pagination
|
|
2302
|
-
{
|
|
2303
|
-
"data": [
|
|
2304
|
-
{ "id": 1, "name": "Item 1" },
|
|
2305
|
-
{ "id": 2, "name": "Item 2" }
|
|
2306
|
-
],
|
|
2307
|
-
"pagination": {
|
|
2308
|
-
"page": 1,
|
|
2309
|
-
"limit": 20,
|
|
2310
|
-
"total": 150,
|
|
2311
|
-
"totalPages": 8
|
|
2312
|
-
}
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
// Error response
|
|
2316
|
-
{
|
|
2317
|
-
"error": {
|
|
2318
|
-
"code": "VALIDATION_ERROR",
|
|
2319
|
-
"message": "The request contains invalid data",
|
|
2320
|
-
"details": [
|
|
2321
|
-
{ "field": "email", "message": "Invalid email format" }
|
|
2322
|
-
]
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
```
|
|
2326
|
-
|
|
2327
|
-
## Hierarchical Resources
|
|
2328
|
-
|
|
2329
|
-
```
|
|
2330
|
-
✅ Limit nesting to 2-3 levels
|
|
2331
|
-
GET /api/v1/authors/456/books # Books by author
|
|
2332
|
-
GET /api/v1/orders/789/items # Items in order
|
|
2333
|
-
|
|
2334
|
-
❌ Too deep
|
|
2335
|
-
GET /api/v1/publishers/1/authors/2/books/3/reviews/4
|
|
2336
|
-
|
|
2337
|
-
✅ Use query parameters instead
|
|
2338
|
-
GET /api/v1/reviews?bookId=3
|
|
2339
|
-
```
|
|
2340
|
-
|
|
2341
|
-
## API Versioning
|
|
2342
|
-
|
|
2343
|
-
```
|
|
2344
|
-
✅ Always version from the start
|
|
2345
|
-
/api/v1/books
|
|
2346
|
-
/api/v2/books
|
|
2347
|
-
|
|
2348
|
-
❌ No version
|
|
2349
|
-
/api/books
|
|
2350
|
-
```
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
# API Pagination
|
|
2354
|
-
|
|
2355
|
-
## Always Paginate Collections
|
|
2356
|
-
|
|
2357
|
-
```typescript
|
|
2358
|
-
// ✅ Paginated endpoint
|
|
2359
|
-
app.get('/api/v1/books', async (req, res) => {
|
|
2360
|
-
const page = parseInt(req.query.page as string) || 1;
|
|
2361
|
-
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
|
2362
|
-
|
|
2363
|
-
const { data, total } = await bookService.findAll({ page, limit });
|
|
2364
|
-
|
|
2365
|
-
res.json({
|
|
2366
|
-
data,
|
|
2367
|
-
pagination: {
|
|
2368
|
-
page,
|
|
2369
|
-
limit,
|
|
2370
|
-
total,
|
|
2371
|
-
totalPages: Math.ceil(total / limit),
|
|
2372
|
-
hasNext: page * limit < total,
|
|
2373
|
-
hasPrevious: page > 1
|
|
2374
|
-
}
|
|
2375
|
-
});
|
|
2376
|
-
});
|
|
2377
|
-
```
|
|
2378
|
-
|
|
2379
|
-
## Offset-Based Pagination
|
|
2380
|
-
|
|
2381
|
-
```typescript
|
|
2382
|
-
// Simple but has issues with large datasets
|
|
2383
|
-
GET /api/v1/books?page=1&limit=20
|
|
2384
|
-
GET /api/v1/books?page=2&limit=20
|
|
2385
|
-
|
|
2386
|
-
// Implementation
|
|
2387
|
-
const getBooks = async (page: number, limit: number) => {
|
|
2388
|
-
const offset = (page - 1) * limit;
|
|
2389
|
-
|
|
2390
|
-
const [data, total] = await Promise.all([
|
|
2391
|
-
db.query('SELECT * FROM books ORDER BY id LIMIT ? OFFSET ?', [limit, offset]),
|
|
2392
|
-
db.query('SELECT COUNT(*) FROM books')
|
|
2393
|
-
]);
|
|
2394
|
-
|
|
2395
|
-
return { data, total };
|
|
2396
|
-
};
|
|
2397
|
-
```
|
|
2398
|
-
|
|
2399
|
-
## Cursor-Based Pagination
|
|
2400
|
-
|
|
2401
|
-
```typescript
|
|
2402
|
-
// Better for large datasets and real-time data
|
|
2403
|
-
GET /api/v1/books?cursor=eyJpZCI6MTIzfQ&limit=20
|
|
2404
|
-
|
|
2405
|
-
// Response includes next cursor
|
|
2406
|
-
{
|
|
2407
|
-
"data": [...],
|
|
2408
|
-
"pagination": {
|
|
2409
|
-
"nextCursor": "eyJpZCI6MTQzfQ",
|
|
2410
|
-
"hasMore": true
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
|
-
// Implementation
|
|
2415
|
-
const getBooks = async (cursor: string | null, limit: number) => {
|
|
2416
|
-
let query = 'SELECT * FROM books';
|
|
2417
|
-
|
|
2418
|
-
if (cursor) {
|
|
2419
|
-
const { id } = decodeCursor(cursor);
|
|
2420
|
-
query += ` WHERE id > ${id}`;
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
query += ` ORDER BY id LIMIT ${limit + 1}`;
|
|
2424
|
-
const data = await db.query(query);
|
|
2425
|
-
|
|
2426
|
-
const hasMore = data.length > limit;
|
|
2427
|
-
const items = hasMore ? data.slice(0, limit) : data;
|
|
2428
|
-
|
|
2429
|
-
return {
|
|
2430
|
-
data: items,
|
|
2431
|
-
pagination: {
|
|
2432
|
-
nextCursor: hasMore ? encodeCursor({ id: items[items.length - 1].id }) : null,
|
|
2433
|
-
hasMore
|
|
2434
|
-
}
|
|
2435
|
-
};
|
|
2436
|
-
};
|
|
2437
|
-
```
|
|
2438
|
-
|
|
2439
|
-
## Keyset Pagination
|
|
2440
|
-
|
|
2441
|
-
```sql
|
|
2442
|
-
-- Most efficient for large tables
|
|
2443
|
-
-- First page
|
|
2444
|
-
SELECT * FROM products
|
|
2445
|
-
ORDER BY created_at DESC, id DESC
|
|
2446
|
-
LIMIT 20;
|
|
2447
|
-
|
|
2448
|
-
-- Next page (using last item's values)
|
|
2449
|
-
SELECT * FROM products
|
|
2450
|
-
WHERE (created_at, id) < ('2024-01-15 10:00:00', 12345)
|
|
2451
|
-
ORDER BY created_at DESC, id DESC
|
|
2452
|
-
LIMIT 20;
|
|
2453
|
-
```
|
|
2454
|
-
|
|
2455
|
-
## HATEOAS Links
|
|
2456
|
-
|
|
2457
|
-
```typescript
|
|
2458
|
-
// Include navigation links
|
|
2459
|
-
{
|
|
2460
|
-
"data": [...],
|
|
2461
|
-
"pagination": {
|
|
2462
|
-
"page": 2,
|
|
2463
|
-
"limit": 20,
|
|
2464
|
-
"total": 150
|
|
2465
|
-
},
|
|
2466
|
-
"links": {
|
|
2467
|
-
"self": "/api/v1/books?page=2&limit=20",
|
|
2468
|
-
"first": "/api/v1/books?page=1&limit=20",
|
|
2469
|
-
"prev": "/api/v1/books?page=1&limit=20",
|
|
2470
|
-
"next": "/api/v1/books?page=3&limit=20",
|
|
2471
|
-
"last": "/api/v1/books?page=8&limit=20"
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
```
|
|
2475
|
-
|
|
2476
|
-
## Pagination Best Practices
|
|
2477
|
-
|
|
2478
|
-
```typescript
|
|
2479
|
-
// ✅ Set reasonable defaults and limits
|
|
2480
|
-
const page = parseInt(req.query.page) || 1;
|
|
2481
|
-
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
|
|
2482
|
-
|
|
2483
|
-
// ✅ Include total count (when practical)
|
|
2484
|
-
const total = await db.count('books');
|
|
2485
|
-
|
|
2486
|
-
// ✅ Use consistent response structure
|
|
2487
|
-
{
|
|
2488
|
-
"data": [],
|
|
2489
|
-
"pagination": { ... }
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
// ❌ Don't return unlimited results
|
|
2493
|
-
// ❌ Don't allow page < 1 or limit < 1
|
|
2494
|
-
```
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
# API Versioning
|
|
2498
|
-
|
|
2499
|
-
## Versioning Strategies
|
|
2500
|
-
|
|
2501
|
-
### URL Path Versioning
|
|
2502
|
-
```
|
|
2503
|
-
GET /api/v1/users
|
|
2504
|
-
GET /api/v2/users
|
|
2505
|
-
```
|
|
2506
|
-
|
|
2507
|
-
### Header Versioning
|
|
2508
|
-
```
|
|
2509
|
-
GET /api/users
|
|
2510
|
-
Accept: application/vnd.api+json; version=2
|
|
2511
|
-
```
|
|
2512
|
-
|
|
2513
|
-
### Query Parameter
|
|
2514
|
-
```
|
|
2515
|
-
GET /api/users?version=2
|
|
2516
|
-
```
|
|
2517
|
-
|
|
2518
|
-
## Implementation
|
|
2519
|
-
|
|
2520
|
-
```typescript
|
|
2521
|
-
// URL path versioning
|
|
2522
|
-
app.use('/api/v1', v1Router);
|
|
2523
|
-
app.use('/api/v2', v2Router);
|
|
2524
|
-
|
|
2525
|
-
// Header versioning middleware
|
|
2526
|
-
function versionMiddleware(req, res, next) {
|
|
2527
|
-
const version = req.headers['api-version'] || '1';
|
|
2528
|
-
req.apiVersion = parseInt(version);
|
|
2529
|
-
next();
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
app.get('/users', versionMiddleware, (req, res) => {
|
|
2533
|
-
if (req.apiVersion >= 2) {
|
|
2534
|
-
return handleV2(req, res);
|
|
2535
|
-
}
|
|
2536
|
-
return handleV1(req, res);
|
|
2537
|
-
});
|
|
2538
|
-
```
|
|
2539
|
-
|
|
2540
|
-
## Deprecation Strategy
|
|
2541
|
-
|
|
2542
|
-
```typescript
|
|
2543
|
-
// Add deprecation headers
|
|
2544
|
-
res.setHeader('Deprecation', 'true');
|
|
2545
|
-
res.setHeader('Sunset', 'Sat, 01 Jan 2025 00:00:00 GMT');
|
|
2546
|
-
res.setHeader('Link', '</api/v2/users>; rel="successor-version"');
|
|
2547
|
-
```
|
|
2548
|
-
|
|
2549
|
-
## Best Practices
|
|
2550
|
-
|
|
2551
|
-
- Version from the start
|
|
2552
|
-
- Support at least N-1 versions
|
|
2553
|
-
- Document deprecation timeline
|
|
2554
|
-
- Provide migration guides
|
|
2555
|
-
- Use semantic versioning for breaking changes
|
|
2556
|
-
- Consider backwards-compatible changes first
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
---
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
## Code Style
|
|
2563
|
-
|
|
2564
|
-
# Naming Conventions
|
|
2565
|
-
|
|
2566
|
-
## Variables and Functions
|
|
2567
|
-
|
|
2568
|
-
```typescript
|
|
2569
|
-
// camelCase for variables and functions
|
|
2570
|
-
const userName = 'John';
|
|
2571
|
-
const isActive = true;
|
|
2572
|
-
const itemCount = 42;
|
|
2573
|
-
|
|
2574
|
-
function calculateTotal(items: Item[]): number {
|
|
2575
|
-
return items.reduce((sum, item) => sum + item.price, 0);
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
|
-
// Boolean variables: use is/has/can/should prefix
|
|
2579
|
-
const isValid = validate(input);
|
|
2580
|
-
const hasPermission = checkPermission(user);
|
|
2581
|
-
const canEdit = user.role === 'admin';
|
|
2582
|
-
const shouldRetry = error.code === 'TIMEOUT';
|
|
2583
|
-
|
|
2584
|
-
// Collections: use plural names
|
|
2585
|
-
const users = getUsers();
|
|
2586
|
-
const activeOrders = orders.filter(o => o.status === 'active');
|
|
2587
|
-
```
|
|
2588
|
-
|
|
2589
|
-
## Constants
|
|
2590
|
-
|
|
2591
|
-
```typescript
|
|
2592
|
-
// UPPER_SNAKE_CASE for constants
|
|
2593
|
-
const MAX_RETRY_ATTEMPTS = 3;
|
|
2594
|
-
const DEFAULT_TIMEOUT_MS = 5000;
|
|
2595
|
-
const API_BASE_URL = 'https://api.example.com';
|
|
2596
|
-
|
|
2597
|
-
// Enum-like objects
|
|
2598
|
-
const ORDER_STATUS = {
|
|
2599
|
-
PENDING: 'pending',
|
|
2600
|
-
PROCESSING: 'processing',
|
|
2601
|
-
SHIPPED: 'shipped',
|
|
2602
|
-
DELIVERED: 'delivered',
|
|
2603
|
-
CANCELLED: 'cancelled'
|
|
2604
|
-
} as const;
|
|
2605
|
-
|
|
2606
|
-
const HTTP_STATUS = {
|
|
2607
|
-
OK: 200,
|
|
2608
|
-
CREATED: 201,
|
|
2609
|
-
BAD_REQUEST: 400,
|
|
2610
|
-
NOT_FOUND: 404
|
|
2611
|
-
} as const;
|
|
2612
|
-
```
|
|
2613
|
-
|
|
2614
|
-
## Classes and Types
|
|
2615
|
-
|
|
2616
|
-
```typescript
|
|
2617
|
-
// PascalCase for classes and types
|
|
2618
|
-
class UserService {
|
|
2619
|
-
constructor(private userRepository: UserRepository) {}
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
interface User {
|
|
2623
|
-
id: string;
|
|
2624
|
-
name: string;
|
|
2625
|
-
email: string;
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
|
-
type UserRole = 'admin' | 'editor' | 'viewer';
|
|
2629
|
-
|
|
2630
|
-
// Avoid prefixes
|
|
2631
|
-
// ❌ IUser, CUser, TUser
|
|
2632
|
-
// ✅ User
|
|
2633
|
-
```
|
|
2634
|
-
|
|
2635
|
-
## Files and Modules
|
|
2636
|
-
|
|
2637
|
-
```typescript
|
|
2638
|
-
// kebab-case for files
|
|
2639
|
-
user-service.ts
|
|
2640
|
-
order-repository.ts
|
|
2641
|
-
create-user.dto.ts
|
|
2642
|
-
|
|
2643
|
-
// Match file name to primary export
|
|
2644
|
-
// user-service.ts exports UserService
|
|
2645
|
-
// order-repository.ts exports OrderRepository
|
|
2646
|
-
```
|
|
2647
|
-
|
|
2648
|
-
## Avoid Bad Names
|
|
2649
|
-
|
|
2650
|
-
```typescript
|
|
2651
|
-
// ❌ Bad - unclear
|
|
2652
|
-
const d = Date.now();
|
|
2653
|
-
const tmp = user.name;
|
|
2654
|
-
const data = fetchData();
|
|
2655
|
-
const flag = true;
|
|
2656
|
-
|
|
2657
|
-
// ✅ Good - descriptive
|
|
2658
|
-
const currentDate = Date.now();
|
|
2659
|
-
const originalUserName = user.name;
|
|
2660
|
-
const customerOrders = fetchCustomerOrders();
|
|
2661
|
-
const isEmailVerified = true;
|
|
2662
|
-
```
|
|
2663
|
-
|
|
2664
|
-
## Avoid Magic Numbers
|
|
2665
|
-
|
|
2666
|
-
```typescript
|
|
2667
|
-
// ❌ Magic numbers
|
|
2668
|
-
if (user.age >= 18) { ... }
|
|
2669
|
-
if (items.length > 100) { ... }
|
|
2670
|
-
setTimeout(callback, 5000);
|
|
2671
|
-
|
|
2672
|
-
// ✅ Named constants
|
|
2673
|
-
const LEGAL_AGE = 18;
|
|
2674
|
-
const MAX_BATCH_SIZE = 100;
|
|
2675
|
-
const DEFAULT_TIMEOUT_MS = 5000;
|
|
2676
|
-
|
|
2677
|
-
if (user.age >= LEGAL_AGE) { ... }
|
|
2678
|
-
if (items.length > MAX_BATCH_SIZE) { ... }
|
|
2679
|
-
setTimeout(callback, DEFAULT_TIMEOUT_MS);
|
|
2680
|
-
```
|
|
2681
|
-
|
|
2682
|
-
## Consistency
|
|
2683
|
-
|
|
2684
|
-
```typescript
|
|
2685
|
-
// Pick ONE style and stick with it across the project
|
|
2686
|
-
|
|
2687
|
-
// ✅ Consistent camelCase in APIs
|
|
2688
|
-
{
|
|
2689
|
-
"userId": 123,
|
|
2690
|
-
"firstName": "John",
|
|
2691
|
-
"createdAt": "2024-01-01"
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
// ❌ Mixed styles
|
|
2695
|
-
{
|
|
2696
|
-
"user_id": 123, // snake_case
|
|
2697
|
-
"firstName": "John", // camelCase - inconsistent!
|
|
2698
|
-
}
|
|
2699
|
-
```
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
# Code Organization
|
|
2703
|
-
|
|
2704
|
-
## Function Length
|
|
2705
|
-
|
|
2706
|
-
```typescript
|
|
2707
|
-
// ❌ Function too long (>50 lines)
|
|
2708
|
-
function processOrder(orderId: string) {
|
|
2709
|
-
// 200 lines of validation, payment, inventory, shipping...
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
// ✅ Extract into smaller, focused functions
|
|
2713
|
-
function processOrder(orderId: string) {
|
|
2714
|
-
const order = fetchOrder(orderId);
|
|
2715
|
-
|
|
2716
|
-
validateOrder(order);
|
|
2717
|
-
reserveInventory(order.items);
|
|
2718
|
-
processPayment(order);
|
|
2719
|
-
scheduleShipping(order);
|
|
2720
|
-
sendConfirmation(order.customer.email);
|
|
2721
|
-
|
|
2722
|
-
return order;
|
|
2723
|
-
}
|
|
2724
|
-
```
|
|
2725
|
-
|
|
2726
|
-
## Nesting Depth
|
|
2727
|
-
|
|
2728
|
-
```typescript
|
|
2729
|
-
// ❌ Too much nesting (>3 levels)
|
|
2730
|
-
if (user) {
|
|
2731
|
-
if (user.isActive) {
|
|
2732
|
-
if (user.hasPermission('edit')) {
|
|
2733
|
-
if (resource.isAvailable) {
|
|
2734
|
-
// Deep nesting is hard to follow
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
// ✅ Guard clauses to reduce nesting
|
|
2741
|
-
if (!user) return;
|
|
2742
|
-
if (!user.isActive) return;
|
|
2743
|
-
if (!user.hasPermission('edit')) return;
|
|
2744
|
-
if (!resource.isAvailable) return;
|
|
2745
|
-
|
|
2746
|
-
// Clear logic at top level
|
|
2747
|
-
|
|
2748
|
-
// ✅ Extract complex conditions
|
|
2749
|
-
function canEditResource(user: User, resource: Resource): boolean {
|
|
2750
|
-
return user &&
|
|
2751
|
-
user.isActive &&
|
|
2752
|
-
user.hasPermission('edit') &&
|
|
2753
|
-
resource.isAvailable;
|
|
2754
|
-
}
|
|
2755
|
-
|
|
2756
|
-
if (canEditResource(user, resource)) {
|
|
2757
|
-
// Single level of nesting
|
|
2758
|
-
}
|
|
2759
|
-
```
|
|
2760
|
-
|
|
2761
|
-
## File Length
|
|
2762
|
-
|
|
2763
|
-
```typescript
|
|
2764
|
-
// ❌ God file (1000+ lines)
|
|
2765
|
-
// user-service.ts with 50 methods handling users, auth, permissions...
|
|
2766
|
-
|
|
2767
|
-
// ✅ Split into focused modules (~200-300 lines each)
|
|
2768
|
-
// user-service.ts - CRUD operations
|
|
2769
|
-
// auth-service.ts - login, logout, tokens
|
|
2770
|
-
// permission-service.ts - role checks
|
|
2771
|
-
```
|
|
2772
|
-
|
|
2773
|
-
## File Organization
|
|
2774
|
-
|
|
2775
|
-
```typescript
|
|
2776
|
-
// Consistent structure within files:
|
|
2777
|
-
|
|
2778
|
-
// 1. Imports (grouped and ordered)
|
|
2779
|
-
import fs from 'fs'; // Standard library
|
|
2780
|
-
import express from 'express'; // External dependencies
|
|
2781
|
-
import { UserService } from './user'; // Internal modules
|
|
2782
|
-
|
|
2783
|
-
// 2. Constants and type definitions
|
|
2784
|
-
const MAX_RETRIES = 3;
|
|
2785
|
-
|
|
2786
|
-
interface UserDTO {
|
|
2787
|
-
id: string;
|
|
2788
|
-
name: string;
|
|
2789
|
-
}
|
|
2790
|
-
|
|
2791
|
-
// 3. Helper functions (if needed)
|
|
2792
|
-
function validateInput(input: unknown): boolean {
|
|
2793
|
-
// ...
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
// 4. Main exports/classes
|
|
2797
|
-
export class OrderService {
|
|
2798
|
-
// ...
|
|
2799
|
-
}
|
|
2800
|
-
|
|
2801
|
-
// 5. Module initialization (if applicable)
|
|
2802
|
-
export default new OrderService();
|
|
2803
|
-
```
|
|
2804
|
-
|
|
2805
|
-
## Single Responsibility
|
|
2806
|
-
|
|
2807
|
-
```typescript
|
|
2808
|
-
// ❌ Class doing too much
|
|
2809
|
-
class UserManager {
|
|
2810
|
-
createUser() {}
|
|
2811
|
-
updateUser() {}
|
|
2812
|
-
sendEmail() {}
|
|
2813
|
-
hashPassword() {}
|
|
2814
|
-
generateToken() {}
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
// ✅ Split by responsibility
|
|
2818
|
-
class UserRepository {
|
|
2819
|
-
create(user: User) {}
|
|
2820
|
-
update(id: string, data: Partial<User>) {}
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
class EmailService {
|
|
2824
|
-
send(to: string, template: string) {}
|
|
2825
|
-
}
|
|
2826
|
-
|
|
2827
|
-
class PasswordService {
|
|
2828
|
-
hash(password: string): string {}
|
|
2829
|
-
verify(password: string, hash: string): boolean {}
|
|
2830
|
-
}
|
|
2831
|
-
|
|
2832
|
-
class AuthService {
|
|
2833
|
-
generateToken(userId: string): string {}
|
|
2834
|
-
}
|
|
2835
|
-
```
|
|
2836
|
-
|
|
2837
|
-
## DRY (Don't Repeat Yourself)
|
|
2838
|
-
|
|
2839
|
-
```typescript
|
|
2840
|
-
// ❌ Duplicated logic
|
|
2841
|
-
function processUserOrder(order: Order) {
|
|
2842
|
-
const total = order.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
|
|
2843
|
-
const tax = total * 0.08;
|
|
2844
|
-
return total + tax;
|
|
2845
|
-
}
|
|
2846
|
-
|
|
2847
|
-
function processGuestOrder(order: Order) {
|
|
2848
|
-
const total = order.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
|
|
2849
|
-
const tax = total * 0.08;
|
|
2850
|
-
return total + tax;
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
// ✅ Extract common logic
|
|
2854
|
-
function calculateOrderTotal(items: Item[]): number {
|
|
2855
|
-
const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
|
|
2856
|
-
const tax = subtotal * 0.08;
|
|
2857
|
-
return subtotal + tax;
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
function processUserOrder(order: Order) {
|
|
2861
|
-
return calculateOrderTotal(order.items);
|
|
2862
|
-
}
|
|
2863
|
-
```
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
---
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
## Error Handling
|
|
2870
|
-
|
|
2871
|
-
# Error Handling Strategy
|
|
2872
|
-
|
|
2873
|
-
## Custom Error Classes
|
|
2874
|
-
|
|
2875
|
-
```typescript
|
|
2876
|
-
class AppError extends Error {
|
|
2877
|
-
constructor(
|
|
2878
|
-
message: string,
|
|
2879
|
-
public statusCode: number = 500,
|
|
2880
|
-
public code: string = 'INTERNAL_ERROR',
|
|
2881
|
-
public details?: unknown
|
|
2882
|
-
) {
|
|
2883
|
-
super(message);
|
|
2884
|
-
this.name = this.constructor.name;
|
|
2885
|
-
Error.captureStackTrace(this, this.constructor);
|
|
2886
|
-
}
|
|
2887
|
-
}
|
|
2888
|
-
|
|
2889
|
-
class NotFoundError extends AppError {
|
|
2890
|
-
constructor(resource: string, id: string) {
|
|
2891
|
-
super(`${resource} with id ${id} not found`, 404, 'NOT_FOUND', { resource, id });
|
|
2892
|
-
}
|
|
2893
|
-
}
|
|
2894
|
-
|
|
2895
|
-
class ValidationError extends AppError {
|
|
2896
|
-
constructor(message: string, details: unknown) {
|
|
2897
|
-
super(message, 400, 'VALIDATION_ERROR', details);
|
|
2898
|
-
}
|
|
2899
|
-
}
|
|
2900
|
-
|
|
2901
|
-
// Usage
|
|
2902
|
-
if (!user) {
|
|
2903
|
-
throw new NotFoundError('User', userId);
|
|
2904
|
-
}
|
|
2905
|
-
```
|
|
2906
|
-
|
|
2907
|
-
## Never Swallow Errors
|
|
2908
|
-
|
|
2909
|
-
```typescript
|
|
2910
|
-
// ❌ Silent failure - dangerous!
|
|
2911
|
-
try {
|
|
2912
|
-
await criticalOperation();
|
|
2913
|
-
} catch (error) {
|
|
2914
|
-
// Error ignored
|
|
2915
|
-
}
|
|
2916
|
-
|
|
2917
|
-
// ✅ Log and handle appropriately
|
|
2918
|
-
try {
|
|
2919
|
-
await criticalOperation();
|
|
2920
|
-
} catch (error) {
|
|
2921
|
-
logger.error('Critical operation failed', { error });
|
|
2922
|
-
throw error; // Or handle gracefully
|
|
2923
|
-
}
|
|
2924
|
-
```
|
|
2925
|
-
|
|
2926
|
-
## Specific Error Messages
|
|
2927
|
-
|
|
2928
|
-
```typescript
|
|
2929
|
-
// ❌ Not actionable
|
|
2930
|
-
throw new Error('Something went wrong');
|
|
2931
|
-
|
|
2932
|
-
// ✅ Specific and actionable
|
|
2933
|
-
throw new ValidationError('Email must be a valid email address', {
|
|
2934
|
-
field: 'email',
|
|
2935
|
-
value: userInput.email
|
|
2936
|
-
});
|
|
2937
|
-
```
|
|
2938
|
-
|
|
2939
|
-
## Centralized Error Handler
|
|
2940
|
-
|
|
2941
|
-
```typescript
|
|
2942
|
-
// Express error middleware
|
|
2943
|
-
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
|
2944
|
-
if (err instanceof AppError) {
|
|
2945
|
-
return res.status(err.statusCode).json({
|
|
2946
|
-
error: {
|
|
2947
|
-
message: err.message,
|
|
2948
|
-
code: err.code,
|
|
2949
|
-
details: err.details
|
|
2950
|
-
}
|
|
2951
|
-
});
|
|
2952
|
-
}
|
|
2953
|
-
|
|
2954
|
-
// Log unexpected errors
|
|
2955
|
-
console.error('Unexpected error:', err);
|
|
2956
|
-
|
|
2957
|
-
// Don't expose internal details
|
|
2958
|
-
res.status(500).json({
|
|
2959
|
-
error: {
|
|
2960
|
-
message: 'Internal server error',
|
|
2961
|
-
code: 'INTERNAL_ERROR'
|
|
2962
|
-
}
|
|
2963
|
-
});
|
|
2964
|
-
});
|
|
2965
|
-
```
|
|
2966
|
-
|
|
2967
|
-
## Async Error Wrapper
|
|
2968
|
-
|
|
2969
|
-
```typescript
|
|
2970
|
-
// Wrap async handlers to catch errors automatically
|
|
2971
|
-
const asyncHandler = (fn: RequestHandler) => {
|
|
2972
|
-
return (req: Request, res: Response, next: NextFunction) => {
|
|
2973
|
-
Promise.resolve(fn(req, res, next)).catch(next);
|
|
2974
|
-
};
|
|
2975
|
-
};
|
|
2976
|
-
|
|
2977
|
-
// Usage
|
|
2978
|
-
app.get('/users/:id', asyncHandler(async (req, res) => {
|
|
2979
|
-
const user = await getUser(req.params.id);
|
|
2980
|
-
res.json(user);
|
|
2981
|
-
}));
|
|
2982
|
-
```
|
|
2983
|
-
|
|
2984
|
-
## Result Type Pattern
|
|
2985
|
-
|
|
2986
|
-
```typescript
|
|
2987
|
-
type Result<T, E = Error> =
|
|
2988
|
-
| { success: true; value: T }
|
|
2989
|
-
| { success: false; error: E };
|
|
2990
|
-
|
|
2991
|
-
function parseJSON<T>(json: string): Result<T, string> {
|
|
2992
|
-
try {
|
|
2993
|
-
return { success: true, value: JSON.parse(json) };
|
|
2994
|
-
} catch {
|
|
2995
|
-
return { success: false, error: 'Invalid JSON' };
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
|
|
2999
|
-
// Usage - forces explicit error handling
|
|
3000
|
-
const result = parseJSON<User>(data);
|
|
3001
|
-
if (result.success) {
|
|
3002
|
-
console.log(result.value.name);
|
|
3003
|
-
} else {
|
|
3004
|
-
console.error(result.error);
|
|
3005
|
-
}
|
|
3006
|
-
```
|
|
3007
|
-
|
|
3008
|
-
## Error Boundaries (React)
|
|
3009
|
-
|
|
3010
|
-
```typescript
|
|
3011
|
-
class ErrorBoundary extends React.Component {
|
|
3012
|
-
state = { hasError: false };
|
|
3013
|
-
|
|
3014
|
-
static getDerivedStateFromError(error: Error) {
|
|
3015
|
-
return { hasError: true };
|
|
3016
|
-
}
|
|
3017
|
-
|
|
3018
|
-
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
3019
|
-
logger.error('UI Error', { error, errorInfo });
|
|
3020
|
-
}
|
|
3021
|
-
|
|
3022
|
-
render() {
|
|
3023
|
-
if (this.state.hasError) {
|
|
3024
|
-
return <FallbackUI />;
|
|
3025
|
-
}
|
|
3026
|
-
return this.props.children;
|
|
3027
|
-
}
|
|
3028
|
-
}
|
|
3029
|
-
```
|
|
3030
|
-
|
|
3031
|
-
## Retry with Limits
|
|
3032
|
-
|
|
3033
|
-
```typescript
|
|
3034
|
-
// ❌ Infinite retries
|
|
3035
|
-
while (true) {
|
|
3036
|
-
try {
|
|
3037
|
-
await operation();
|
|
3038
|
-
break;
|
|
3039
|
-
} catch (error) {
|
|
3040
|
-
// Retry forever - exhausts resources
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
// ✅ Limited retries with backoff
|
|
3045
|
-
const maxRetries = 3;
|
|
3046
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
3047
|
-
try {
|
|
3048
|
-
await operation();
|
|
3049
|
-
break;
|
|
3050
|
-
} catch (error) {
|
|
3051
|
-
if (i === maxRetries - 1) throw error;
|
|
3052
|
-
await sleep(Math.pow(2, i) * 1000); // Exponential backoff
|
|
3053
|
-
}
|
|
3054
|
-
}
|
|
3055
|
-
```
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
# Error Handling Basics
|
|
3059
|
-
|
|
3060
|
-
## Throwing Errors
|
|
3061
|
-
|
|
3062
|
-
Use `throw` to signal when something goes wrong.
|
|
3063
|
-
|
|
3064
|
-
```pseudocode
|
|
3065
|
-
function divide(a, b):
|
|
3066
|
-
if b == 0:
|
|
3067
|
-
throw Error("Cannot divide by zero")
|
|
3068
|
-
return a / b
|
|
3069
|
-
```
|
|
3070
|
-
|
|
3071
|
-
## Try-Catch
|
|
3072
|
-
|
|
3073
|
-
Use `try-catch` to handle errors gracefully.
|
|
3074
|
-
|
|
3075
|
-
```pseudocode
|
|
3076
|
-
try:
|
|
3077
|
-
result = divide(10, 0)
|
|
3078
|
-
print(result)
|
|
3079
|
-
catch error:
|
|
3080
|
-
print("Error:", error.message)
|
|
3081
|
-
// Continue with fallback behavior
|
|
3082
|
-
```
|
|
3083
|
-
|
|
3084
|
-
## Always Provide Error Messages
|
|
3085
|
-
|
|
3086
|
-
Make error messages clear and actionable.
|
|
3087
|
-
|
|
3088
|
-
```pseudocode
|
|
3089
|
-
// ❌ Bad: Vague error
|
|
3090
|
-
throw Error("Invalid")
|
|
3091
|
-
|
|
3092
|
-
// ✅ Good: Specific error
|
|
3093
|
-
throw Error("Email must be a valid email address")
|
|
3094
|
-
|
|
3095
|
-
// ✅ Good: Include context
|
|
3096
|
-
throw Error("User with ID " + userId + " not found")
|
|
3097
|
-
```
|
|
3098
|
-
|
|
3099
|
-
## Async Error Handling
|
|
3100
|
-
|
|
3101
|
-
Always use try-catch with async operations.
|
|
3102
|
-
|
|
3103
|
-
```pseudocode
|
|
3104
|
-
async function loadUser(id):
|
|
3105
|
-
try:
|
|
3106
|
-
user = await fetchUser(id)
|
|
3107
|
-
return user
|
|
3108
|
-
catch error:
|
|
3109
|
-
log("Failed to load user:", error)
|
|
3110
|
-
throw error // Re-throw if caller should know
|
|
3111
|
-
```
|
|
3112
|
-
|
|
3113
|
-
## When to Catch Errors
|
|
3114
|
-
|
|
3115
|
-
### Catch When You Can Handle It
|
|
3116
|
-
|
|
3117
|
-
```pseudocode
|
|
3118
|
-
// ✅ Good: Can provide fallback
|
|
3119
|
-
async function getUserName(id):
|
|
3120
|
-
try:
|
|
3121
|
-
user = await fetchUser(id)
|
|
3122
|
-
return user.name
|
|
3123
|
-
catch error:
|
|
3124
|
-
return "Unknown User" // Fallback value
|
|
3125
|
-
```
|
|
3126
|
-
|
|
3127
|
-
### Don't Catch If You Can't Handle
|
|
3128
|
-
|
|
3129
|
-
```pseudocode
|
|
3130
|
-
// ❌ Bad: Catching but doing nothing
|
|
3131
|
-
try:
|
|
3132
|
-
await criticalOperation()
|
|
3133
|
-
catch error:
|
|
3134
|
-
// Empty catch - error lost!
|
|
3135
|
-
|
|
3136
|
-
// ✅ Good: Let it propagate
|
|
3137
|
-
await criticalOperation() // Caller will handle
|
|
3138
|
-
```
|
|
3139
|
-
|
|
3140
|
-
## Validate Input Early
|
|
3141
|
-
|
|
3142
|
-
Check for problems before they cause errors.
|
|
3143
|
-
|
|
3144
|
-
```pseudocode
|
|
3145
|
-
function createUser(email, age):
|
|
3146
|
-
// Validate inputs first
|
|
3147
|
-
if email is empty:
|
|
3148
|
-
throw Error("Email is required")
|
|
3149
|
-
|
|
3150
|
-
if not email.contains("@"):
|
|
3151
|
-
throw Error("Email must be valid")
|
|
3152
|
-
|
|
3153
|
-
if age < 0:
|
|
3154
|
-
throw Error("Age cannot be negative")
|
|
3155
|
-
|
|
3156
|
-
// Now proceed with creation
|
|
3157
|
-
return { email: email, age: age }
|
|
3158
|
-
```
|
|
3159
|
-
|
|
3160
|
-
## Logging Errors
|
|
3161
|
-
|
|
3162
|
-
Always log errors for debugging.
|
|
3163
|
-
|
|
3164
|
-
```pseudocode
|
|
3165
|
-
try:
|
|
3166
|
-
await processPayment(orderId)
|
|
3167
|
-
catch error:
|
|
3168
|
-
// Log with context
|
|
3169
|
-
log("Payment processing failed:", {
|
|
3170
|
-
orderId: orderId,
|
|
3171
|
-
error: error.message,
|
|
3172
|
-
timestamp: currentTime()
|
|
3173
|
-
})
|
|
3174
|
-
throw error
|
|
3175
|
-
```
|
|
3176
|
-
|
|
3177
|
-
## Error Handling Patterns
|
|
3178
|
-
|
|
3179
|
-
### Return Error Status
|
|
3180
|
-
|
|
3181
|
-
```pseudocode
|
|
3182
|
-
function parseNumber(input):
|
|
3183
|
-
num = parseInt(input)
|
|
3184
|
-
if isNaN(num):
|
|
3185
|
-
return null // Indicate failure without throwing
|
|
3186
|
-
return num
|
|
3187
|
-
|
|
3188
|
-
// Usage
|
|
3189
|
-
result = parseNumber(userInput)
|
|
3190
|
-
if result is null:
|
|
3191
|
-
print("Invalid number")
|
|
3192
|
-
else:
|
|
3193
|
-
print("Number:", result)
|
|
3194
|
-
```
|
|
3195
|
-
|
|
3196
|
-
### Return Success/Error Object
|
|
3197
|
-
|
|
3198
|
-
```pseudocode
|
|
3199
|
-
function safeDivide(a, b):
|
|
3200
|
-
if b == 0:
|
|
3201
|
-
return { success: false, error: "Cannot divide by zero" }
|
|
3202
|
-
return { success: true, data: a / b }
|
|
3203
|
-
|
|
3204
|
-
// Usage
|
|
3205
|
-
result = safeDivide(10, 2)
|
|
3206
|
-
if result.success:
|
|
3207
|
-
print("Result:", result.data)
|
|
3208
|
-
else:
|
|
3209
|
-
print("Error:", result.error)
|
|
3210
|
-
```
|
|
3211
|
-
|
|
3212
|
-
## Common Mistakes
|
|
3213
|
-
|
|
3214
|
-
### Don't Swallow Errors
|
|
3215
|
-
|
|
3216
|
-
```pseudocode
|
|
3217
|
-
// ❌ Bad: Error disappears
|
|
3218
|
-
try:
|
|
3219
|
-
await importantOperation()
|
|
3220
|
-
catch error:
|
|
3221
|
-
// Nothing here - error lost!
|
|
3222
|
-
|
|
3223
|
-
// ✅ Good: Log at minimum
|
|
3224
|
-
try:
|
|
3225
|
-
await importantOperation()
|
|
3226
|
-
catch error:
|
|
3227
|
-
log("Operation failed:", error)
|
|
3228
|
-
throw error // Or handle appropriately
|
|
3229
|
-
```
|
|
3230
|
-
|
|
3231
|
-
### Don't Use Errors for Flow Control
|
|
3232
|
-
|
|
3233
|
-
```pseudocode
|
|
3234
|
-
// ❌ Bad: Using errors for normal logic
|
|
3235
|
-
try:
|
|
3236
|
-
user = findUser(id)
|
|
3237
|
-
// ...
|
|
3238
|
-
catch error:
|
|
3239
|
-
// User not found - this is expected, not an error!
|
|
3240
|
-
|
|
3241
|
-
// ✅ Good: Return null for expected cases
|
|
3242
|
-
user = findUser(id)
|
|
3243
|
-
if user is null:
|
|
3244
|
-
print("User not found")
|
|
3245
|
-
return
|
|
3246
|
-
```
|
|
3247
|
-
|
|
3248
|
-
### Don't Throw Non-Error Objects
|
|
3249
|
-
|
|
3250
|
-
```pseudocode
|
|
3251
|
-
// ❌ Bad
|
|
3252
|
-
throw "Something went wrong" // String
|
|
3253
|
-
throw { code: 500 } // Plain object
|
|
3254
|
-
|
|
3255
|
-
// ✅ Good
|
|
3256
|
-
throw Error("Something went wrong")
|
|
3257
|
-
```
|
|
3258
|
-
|
|
3259
|
-
## Best Practices
|
|
3260
|
-
|
|
3261
|
-
1. **Fail fast** - Validate input early and throw immediately
|
|
3262
|
-
2. **Be specific** - Provide detailed, actionable error messages
|
|
3263
|
-
3. **Log errors** - Always log for debugging
|
|
3264
|
-
4. **Don't hide errors** - Let them propagate if you can't handle them
|
|
3265
|
-
5. **Clean up resources** - Close connections, files in finally blocks
|
|
3266
|
-
|
|
3267
|
-
```pseudocode
|
|
3268
|
-
file = null
|
|
3269
|
-
try:
|
|
3270
|
-
file = await openFile("data.txt")
|
|
3271
|
-
await processFile(file)
|
|
3272
|
-
catch error:
|
|
3273
|
-
log("File processing failed:", error)
|
|
3274
|
-
throw error
|
|
3275
|
-
finally:
|
|
3276
|
-
// Always runs, even if error thrown
|
|
3277
|
-
if file is not null:
|
|
3278
|
-
await file.close()
|
|
3279
|
-
```
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
---
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
## Architecture
|
|
3286
|
-
|
|
3287
|
-
# SOLID Principles
|
|
3288
|
-
|
|
3289
|
-
## Single Responsibility Principle (SRP)
|
|
3290
|
-
|
|
3291
|
-
A class should have only one reason to change.
|
|
3292
|
-
|
|
3293
|
-
**Bad:**
|
|
3294
|
-
```typescript
|
|
3295
|
-
class UserService {
|
|
3296
|
-
createUser(data: UserData): User { /* ... */ }
|
|
3297
|
-
sendWelcomeEmail(user: User): void { /* ... */ }
|
|
3298
|
-
generateReport(users: User[]): Report { /* ... */ }
|
|
3299
|
-
}
|
|
3300
|
-
```
|
|
3301
|
-
|
|
3302
|
-
**Good:**
|
|
3303
|
-
```typescript
|
|
3304
|
-
class UserService {
|
|
3305
|
-
createUser(data: UserData): User { /* ... */ }
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3308
|
-
class EmailService {
|
|
3309
|
-
sendWelcomeEmail(user: User): void { /* ... */ }
|
|
3310
|
-
}
|
|
3311
|
-
|
|
3312
|
-
class ReportService {
|
|
3313
|
-
generateUserReport(users: User[]): Report { /* ... */ }
|
|
3314
|
-
}
|
|
3315
|
-
```
|
|
3316
|
-
|
|
3317
|
-
## Open/Closed Principle (OCP)
|
|
3318
|
-
|
|
3319
|
-
Open for extension, closed for modification.
|
|
3320
|
-
|
|
3321
|
-
**Bad:**
|
|
3322
|
-
```typescript
|
|
3323
|
-
class PaymentProcessor {
|
|
3324
|
-
process(payment: Payment): void {
|
|
3325
|
-
if (payment.type === 'credit') { /* credit logic */ }
|
|
3326
|
-
else if (payment.type === 'paypal') { /* paypal logic */ }
|
|
3327
|
-
// Must modify class to add new payment types
|
|
3328
|
-
}
|
|
3329
|
-
}
|
|
3330
|
-
```
|
|
3331
|
-
|
|
3332
|
-
**Good:**
|
|
3333
|
-
```typescript
|
|
3334
|
-
interface PaymentHandler {
|
|
3335
|
-
process(payment: Payment): void;
|
|
3336
|
-
}
|
|
3337
|
-
|
|
3338
|
-
class CreditCardHandler implements PaymentHandler {
|
|
3339
|
-
process(payment: Payment): void { /* credit logic */ }
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
|
-
class PayPalHandler implements PaymentHandler {
|
|
3343
|
-
process(payment: Payment): void { /* paypal logic */ }
|
|
3344
|
-
}
|
|
3345
|
-
|
|
3346
|
-
class PaymentProcessor {
|
|
3347
|
-
constructor(private handlers: Map<string, PaymentHandler>) {}
|
|
3348
|
-
|
|
3349
|
-
process(payment: Payment): void {
|
|
3350
|
-
this.handlers.get(payment.type)?.process(payment);
|
|
3351
|
-
}
|
|
3352
|
-
}
|
|
3353
|
-
```
|
|
3354
|
-
|
|
3355
|
-
## Liskov Substitution Principle (LSP)
|
|
3356
|
-
|
|
3357
|
-
Subtypes must be substitutable for their base types.
|
|
3358
|
-
|
|
3359
|
-
**Bad:**
|
|
3360
|
-
```typescript
|
|
3361
|
-
class Bird {
|
|
3362
|
-
fly(): void { /* flying logic */ }
|
|
3363
|
-
}
|
|
3364
|
-
|
|
3365
|
-
class Penguin extends Bird {
|
|
3366
|
-
fly(): void {
|
|
3367
|
-
throw new Error("Penguins can't fly!"); // Violates LSP
|
|
3368
|
-
}
|
|
3369
|
-
}
|
|
3370
|
-
```
|
|
3371
|
-
|
|
3372
|
-
**Good:**
|
|
3373
|
-
```typescript
|
|
3374
|
-
interface Bird {
|
|
3375
|
-
move(): void;
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
|
-
class FlyingBird implements Bird {
|
|
3379
|
-
move(): void { this.fly(); }
|
|
3380
|
-
private fly(): void { /* flying logic */ }
|
|
3381
|
-
}
|
|
3382
|
-
|
|
3383
|
-
class Penguin implements Bird {
|
|
3384
|
-
move(): void { this.swim(); }
|
|
3385
|
-
private swim(): void { /* swimming logic */ }
|
|
3386
|
-
}
|
|
3387
|
-
```
|
|
3388
|
-
|
|
3389
|
-
## Interface Segregation Principle (ISP)
|
|
3390
|
-
|
|
3391
|
-
Clients shouldn't depend on interfaces they don't use.
|
|
3392
|
-
|
|
3393
|
-
**Bad:**
|
|
3394
|
-
```typescript
|
|
3395
|
-
interface Worker {
|
|
3396
|
-
work(): void;
|
|
3397
|
-
eat(): void;
|
|
3398
|
-
sleep(): void;
|
|
3399
|
-
}
|
|
3400
|
-
|
|
3401
|
-
class Robot implements Worker {
|
|
3402
|
-
work(): void { /* ... */ }
|
|
3403
|
-
eat(): void { throw new Error("Robots don't eat"); }
|
|
3404
|
-
sleep(): void { throw new Error("Robots don't sleep"); }
|
|
3405
|
-
}
|
|
3406
|
-
```
|
|
3407
|
-
|
|
3408
|
-
**Good:**
|
|
3409
|
-
```typescript
|
|
3410
|
-
interface Workable {
|
|
3411
|
-
work(): void;
|
|
3412
|
-
}
|
|
3413
|
-
|
|
3414
|
-
interface Eatable {
|
|
3415
|
-
eat(): void;
|
|
3416
|
-
}
|
|
3417
|
-
|
|
3418
|
-
interface Sleepable {
|
|
3419
|
-
sleep(): void;
|
|
3420
|
-
}
|
|
3421
|
-
|
|
3422
|
-
class Human implements Workable, Eatable, Sleepable {
|
|
3423
|
-
work(): void { /* ... */ }
|
|
3424
|
-
eat(): void { /* ... */ }
|
|
3425
|
-
sleep(): void { /* ... */ }
|
|
3426
|
-
}
|
|
3427
|
-
|
|
3428
|
-
class Robot implements Workable {
|
|
3429
|
-
work(): void { /* ... */ }
|
|
3430
|
-
}
|
|
3431
|
-
```
|
|
3432
|
-
|
|
3433
|
-
## Dependency Inversion Principle (DIP)
|
|
3434
|
-
|
|
3435
|
-
Depend on abstractions, not concretions.
|
|
3436
|
-
|
|
3437
|
-
**Bad:**
|
|
3438
|
-
```typescript
|
|
3439
|
-
class UserService {
|
|
3440
|
-
private database = new MySQLDatabase();
|
|
3441
|
-
|
|
3442
|
-
getUser(id: string): User {
|
|
3443
|
-
return this.database.query(`SELECT * FROM users WHERE id = '${id}'`);
|
|
3444
|
-
}
|
|
3445
|
-
}
|
|
3446
|
-
```
|
|
3447
|
-
|
|
3448
|
-
**Good:**
|
|
3449
|
-
```typescript
|
|
3450
|
-
interface Database {
|
|
3451
|
-
query(sql: string): any;
|
|
3452
|
-
}
|
|
3453
|
-
|
|
3454
|
-
class UserService {
|
|
3455
|
-
constructor(private database: Database) {}
|
|
3456
|
-
|
|
3457
|
-
getUser(id: string): User {
|
|
3458
|
-
return this.database.query(`SELECT * FROM users WHERE id = '${id}'`);
|
|
3459
|
-
}
|
|
3460
|
-
}
|
|
3461
|
-
|
|
3462
|
-
// Can inject any database implementation
|
|
3463
|
-
const userService = new UserService(new MySQLDatabase());
|
|
3464
|
-
const testService = new UserService(new InMemoryDatabase());
|
|
3465
|
-
```
|
|
3466
|
-
|
|
3467
|
-
## Best Practices
|
|
3468
|
-
|
|
3469
|
-
- Apply SRP at class, method, and module levels
|
|
3470
|
-
- Use interfaces and dependency injection for flexibility
|
|
3471
|
-
- Prefer composition over inheritance
|
|
3472
|
-
- Design small, focused interfaces
|
|
3473
|
-
- Inject dependencies rather than creating them internally
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
# GUI Architecture Patterns
|
|
3477
|
-
|
|
3478
|
-
## MVC (Model-View-Controller)
|
|
3479
|
-
|
|
3480
|
-
```typescript
|
|
3481
|
-
// Model - data and business logic
|
|
3482
|
-
class UserModel {
|
|
3483
|
-
private users: User[] = [];
|
|
3484
|
-
|
|
3485
|
-
getUsers(): User[] { return this.users; }
|
|
3486
|
-
addUser(user: User): void { this.users.push(user); }
|
|
3487
|
-
}
|
|
3488
|
-
|
|
3489
|
-
// View - presentation
|
|
3490
|
-
class UserView {
|
|
3491
|
-
render(users: User[]): void {
|
|
3492
|
-
console.log('Users:', users);
|
|
3493
|
-
}
|
|
3494
|
-
}
|
|
3495
|
-
|
|
3496
|
-
// Controller - handles input, coordinates
|
|
3497
|
-
class UserController {
|
|
3498
|
-
constructor(
|
|
3499
|
-
private model: UserModel,
|
|
3500
|
-
private view: UserView
|
|
3501
|
-
) {}
|
|
3502
|
-
|
|
3503
|
-
handleAddUser(userData: UserData): void {
|
|
3504
|
-
const user = new User(userData);
|
|
3505
|
-
this.model.addUser(user);
|
|
3506
|
-
this.view.render(this.model.getUsers());
|
|
3507
|
-
}
|
|
3508
|
-
}
|
|
3509
|
-
```
|
|
3510
|
-
|
|
3511
|
-
## MVP (Model-View-Presenter)
|
|
3512
|
-
|
|
3513
|
-
```typescript
|
|
3514
|
-
// View interface - defines what presenter can call
|
|
3515
|
-
interface UserView {
|
|
3516
|
-
showUsers(users: User[]): void;
|
|
3517
|
-
showError(message: string): void;
|
|
3518
|
-
}
|
|
3519
|
-
|
|
3520
|
-
// Presenter - all presentation logic
|
|
3521
|
-
class UserPresenter {
|
|
3522
|
-
constructor(
|
|
3523
|
-
private view: UserView,
|
|
3524
|
-
private model: UserModel
|
|
3525
|
-
) {}
|
|
3526
|
-
|
|
3527
|
-
loadUsers(): void {
|
|
3528
|
-
try {
|
|
3529
|
-
const users = this.model.getUsers();
|
|
3530
|
-
this.view.showUsers(users);
|
|
3531
|
-
} catch (error) {
|
|
3532
|
-
this.view.showError('Failed to load users');
|
|
3533
|
-
}
|
|
3534
|
-
}
|
|
3535
|
-
}
|
|
3536
|
-
|
|
3537
|
-
// View implementation - passive, no logic
|
|
3538
|
-
class UserListView implements UserView {
|
|
3539
|
-
showUsers(users: User[]): void { /* render list */ }
|
|
3540
|
-
showError(message: string): void { /* show error */ }
|
|
3541
|
-
}
|
|
3542
|
-
```
|
|
3543
|
-
|
|
3544
|
-
## MVVM (Model-View-ViewModel)
|
|
3545
|
-
|
|
3546
|
-
```typescript
|
|
3547
|
-
// ViewModel - exposes observable state
|
|
3548
|
-
class UserViewModel {
|
|
3549
|
-
users = observable<User[]>([]);
|
|
3550
|
-
isLoading = observable(false);
|
|
3551
|
-
|
|
3552
|
-
async loadUsers(): Promise<void> {
|
|
3553
|
-
this.isLoading.set(true);
|
|
3554
|
-
const users = await this.userService.getUsers();
|
|
3555
|
-
this.users.set(users);
|
|
3556
|
-
this.isLoading.set(false);
|
|
3557
|
-
}
|
|
3558
|
-
}
|
|
3559
|
-
|
|
3560
|
-
// View binds to ViewModel
|
|
3561
|
-
const UserList = observer(({ viewModel }: { viewModel: UserViewModel }) => (
|
|
3562
|
-
<div>
|
|
3563
|
-
{viewModel.isLoading.get() ? (
|
|
3564
|
-
<Spinner />
|
|
3565
|
-
) : (
|
|
3566
|
-
viewModel.users.get().map(user => <UserItem key={user.id} user={user} />)
|
|
3567
|
-
)}
|
|
3568
|
-
</div>
|
|
3569
|
-
));
|
|
3570
|
-
```
|
|
3571
|
-
|
|
3572
|
-
## Component Architecture (React/Vue)
|
|
3573
|
-
|
|
3574
|
-
```typescript
|
|
3575
|
-
// Presentational component - no state, just props
|
|
3576
|
-
const UserCard = ({ user, onDelete }: UserCardProps) => (
|
|
3577
|
-
<div className="user-card">
|
|
3578
|
-
<h3>{user.name}</h3>
|
|
3579
|
-
<button onClick={() => onDelete(user.id)}>Delete</button>
|
|
3580
|
-
</div>
|
|
3581
|
-
);
|
|
3582
|
-
|
|
3583
|
-
// Container component - manages state
|
|
3584
|
-
const UserListContainer = () => {
|
|
3585
|
-
const [users, setUsers] = useState<User[]>([]);
|
|
3586
|
-
|
|
3587
|
-
useEffect(() => {
|
|
3588
|
-
userService.getUsers().then(setUsers);
|
|
3589
|
-
}, []);
|
|
3590
|
-
|
|
3591
|
-
const handleDelete = (id: string) => {
|
|
3592
|
-
userService.deleteUser(id).then(() => {
|
|
3593
|
-
setUsers(users.filter(u => u.id !== id));
|
|
3594
|
-
});
|
|
3595
|
-
};
|
|
3596
|
-
|
|
3597
|
-
return <UserList users={users} onDelete={handleDelete} />;
|
|
3598
|
-
};
|
|
3599
|
-
```
|
|
3600
|
-
|
|
3601
|
-
## Best Practices
|
|
3602
|
-
|
|
3603
|
-
- Separate UI logic from business logic
|
|
3604
|
-
- Keep views as simple as possible
|
|
3605
|
-
- Use unidirectional data flow when possible
|
|
3606
|
-
- Make components reusable and testable
|
|
3607
|
-
- Choose pattern based on framework and team familiarity
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
# Feature Toggles
|
|
3611
|
-
|
|
3612
|
-
## Toggle Types
|
|
3613
|
-
|
|
3614
|
-
### Release Toggles
|
|
3615
|
-
Hide incomplete features in production.
|
|
3616
|
-
|
|
3617
|
-
```typescript
|
|
3618
|
-
if (featureFlags.isEnabled('new-checkout')) {
|
|
3619
|
-
return <NewCheckout />;
|
|
3620
|
-
}
|
|
3621
|
-
return <LegacyCheckout />;
|
|
3622
|
-
```
|
|
3623
|
-
|
|
3624
|
-
### Experiment Toggles
|
|
3625
|
-
A/B testing and gradual rollouts.
|
|
3626
|
-
|
|
3627
|
-
```typescript
|
|
3628
|
-
const variant = featureFlags.getVariant('pricing-experiment', userId);
|
|
3629
|
-
if (variant === 'new-pricing') {
|
|
3630
|
-
return calculateNewPricing(cart);
|
|
3631
|
-
}
|
|
3632
|
-
return calculateLegacyPricing(cart);
|
|
3633
|
-
```
|
|
3634
|
-
|
|
3635
|
-
### Ops Toggles
|
|
3636
|
-
Runtime operational control.
|
|
3637
|
-
|
|
3638
|
-
```typescript
|
|
3639
|
-
if (featureFlags.isEnabled('enable-caching')) {
|
|
3640
|
-
return cache.get(key) || fetchFromDatabase(key);
|
|
3641
|
-
}
|
|
3642
|
-
return fetchFromDatabase(key);
|
|
3643
|
-
```
|
|
3644
|
-
|
|
3645
|
-
## Implementation
|
|
3646
|
-
|
|
3647
|
-
```typescript
|
|
3648
|
-
interface FeatureFlags {
|
|
3649
|
-
isEnabled(flag: string, context?: Context): boolean;
|
|
3650
|
-
getVariant(flag: string, userId: string): string;
|
|
3651
|
-
}
|
|
3652
|
-
|
|
3653
|
-
class FeatureFlagService implements FeatureFlags {
|
|
3654
|
-
constructor(private config: Map<string, FlagConfig>) {}
|
|
3655
|
-
|
|
3656
|
-
isEnabled(flag: string, context?: Context): boolean {
|
|
3657
|
-
const config = this.config.get(flag);
|
|
3658
|
-
if (!config) return false;
|
|
3659
|
-
|
|
3660
|
-
if (config.percentage) {
|
|
3661
|
-
return this.isInPercentage(context?.userId, config.percentage);
|
|
3662
|
-
}
|
|
3663
|
-
|
|
3664
|
-
return config.enabled;
|
|
3665
|
-
}
|
|
3666
|
-
|
|
3667
|
-
private isInPercentage(userId: string | undefined, percentage: number): boolean {
|
|
3668
|
-
if (!userId) return false;
|
|
3669
|
-
const hash = this.hashUserId(userId);
|
|
3670
|
-
return (hash % 100) < percentage;
|
|
3671
|
-
}
|
|
3672
|
-
}
|
|
3673
|
-
```
|
|
3674
|
-
|
|
3675
|
-
## Best Practices
|
|
3676
|
-
|
|
3677
|
-
- Remove toggles after feature is stable
|
|
3678
|
-
- Use clear naming conventions
|
|
3679
|
-
- Log toggle decisions for debugging
|
|
3680
|
-
- Test both toggle states
|
|
3681
|
-
- Limit number of active toggles
|
|
3682
|
-
- Document toggle purpose and expiration
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
---
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
## DevOps
|
|
3689
|
-
|
|
3690
|
-
# CI/CD Practices
|
|
3691
|
-
|
|
3692
|
-
## Continuous Integration
|
|
3693
|
-
|
|
3694
|
-
Run on every commit:
|
|
3695
|
-
|
|
3696
|
-
```yaml
|
|
3697
|
-
# .github/workflows/ci.yml
|
|
3698
|
-
name: CI
|
|
3699
|
-
on: [push, pull_request]
|
|
3700
|
-
|
|
3701
|
-
jobs:
|
|
3702
|
-
build:
|
|
3703
|
-
runs-on: ubuntu-latest
|
|
3704
|
-
steps:
|
|
3705
|
-
- uses: actions/checkout@v4
|
|
3706
|
-
- uses: actions/setup-node@v4
|
|
3707
|
-
with:
|
|
3708
|
-
node-version: '20'
|
|
3709
|
-
cache: 'npm'
|
|
3710
|
-
- run: npm ci
|
|
3711
|
-
- run: npm run lint
|
|
3712
|
-
- run: npm run typecheck
|
|
3713
|
-
- run: npm test
|
|
3714
|
-
- run: npm run build
|
|
3715
|
-
```
|
|
3716
|
-
|
|
3717
|
-
## Continuous Deployment
|
|
3718
|
-
|
|
3719
|
-
Deploy automatically after CI passes:
|
|
3720
|
-
|
|
3721
|
-
```yaml
|
|
3722
|
-
deploy:
|
|
3723
|
-
needs: build
|
|
3724
|
-
if: github.ref == 'refs/heads/main'
|
|
3725
|
-
runs-on: ubuntu-latest
|
|
3726
|
-
steps:
|
|
3727
|
-
- uses: actions/checkout@v4
|
|
3728
|
-
- run: npm ci
|
|
3729
|
-
- run: npm run build
|
|
3730
|
-
- run: npm run deploy
|
|
3731
|
-
env:
|
|
3732
|
-
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
|
3733
|
-
```
|
|
3734
|
-
|
|
3735
|
-
## Deployment Strategies
|
|
3736
|
-
|
|
3737
|
-
### Blue-Green Deployment
|
|
3738
|
-
Run two identical environments, switch traffic instantly.
|
|
3739
|
-
|
|
3740
|
-
### Canary Releases
|
|
3741
|
-
Route small percentage of traffic to new version first.
|
|
3742
|
-
|
|
3743
|
-
### Rolling Updates
|
|
3744
|
-
Gradually replace instances with new version.
|
|
3745
|
-
|
|
3746
|
-
## Best Practices
|
|
3747
|
-
|
|
3748
|
-
- Run fast tests first, slow tests later
|
|
3749
|
-
- Cache dependencies between runs
|
|
3750
|
-
- Use matrix builds for multiple versions/platforms
|
|
3751
|
-
- Keep secrets in secure storage
|
|
3752
|
-
- Automate database migrations
|
|
3753
|
-
- Include rollback procedures
|
|
3754
|
-
- Monitor deployments with health checks
|
|
3755
|
-
- Use feature flags for safer releases
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
# Observability
|
|
3759
|
-
|
|
3760
|
-
## Overview
|
|
3761
|
-
|
|
3762
|
-
Observability is the ability to understand the internal state of a system by examining its outputs. In modern distributed systems, it goes beyond simple monitoring to include logging, metrics, and tracing.
|
|
3763
|
-
|
|
3764
|
-
## Three Pillars
|
|
3765
|
-
|
|
3766
|
-
### 1. Structured Logging
|
|
3767
|
-
|
|
3768
|
-
Logs should be machine-readable (JSON) and contain context.
|
|
3769
|
-
|
|
3770
|
-
```json
|
|
3771
|
-
// ✅ Good: Structured JSON logging
|
|
3772
|
-
{
|
|
3773
|
-
"level": "info",
|
|
3774
|
-
"message": "Order processed",
|
|
3775
|
-
"orderId": "ord_123",
|
|
3776
|
-
"userId": "user_456",
|
|
3777
|
-
"amount": 99.99,
|
|
3778
|
-
"durationMs": 145,
|
|
3779
|
-
"status": "success"
|
|
3780
|
-
}
|
|
3781
|
-
```
|
|
3782
|
-
```text
|
|
3783
|
-
// ❌ Bad: Unstructured text
|
|
3784
|
-
"Order processed: ord_123 for user_456"
|
|
3785
|
-
```
|
|
3786
|
-
|
|
3787
|
-
### 2. Metrics
|
|
3788
|
-
|
|
3789
|
-
Aggregatable data points for identifying trends and health.
|
|
3790
|
-
|
|
3791
|
-
- **Counters**: Total requests, error counts (`http_requests_total`)
|
|
3792
|
-
- **Gauges**: Queue depth, memory usage (`memory_usage_bytes`)
|
|
3793
|
-
- **Histograms**: Request latency distribution (`http_request_duration_seconds`)
|
|
3794
|
-
|
|
3795
|
-
### 3. Distributed Tracing
|
|
3796
|
-
|
|
3797
|
-
Tracking requests as they propagate through services.
|
|
3798
|
-
|
|
3799
|
-
- **Trace ID**: Unique ID for the entire request chain
|
|
3800
|
-
- **Span ID**: Unique ID for a specific operation
|
|
3801
|
-
- **Context Propagation**: Passing IDs between services (e.g., W3C Trace Context)
|
|
3802
|
-
|
|
3803
|
-
## Implementation Strategy
|
|
3804
|
-
|
|
3805
|
-
### OpenTelemetry (OTel)
|
|
3806
|
-
|
|
3807
|
-
Use OpenTelemetry as the vendor-neutral standard for collecting telemetry data.
|
|
3808
|
-
|
|
3809
|
-
```text
|
|
3810
|
-
# Initialize OpenTelemetry SDK
|
|
3811
|
-
SDK.Configure({
|
|
3812
|
-
TraceExporter: Console/OTLP,
|
|
3813
|
-
Instrumentations: [Http, Database, Grpc]
|
|
3814
|
-
})
|
|
3815
|
-
SDK.Start()
|
|
3816
|
-
```
|
|
3817
|
-
|
|
3818
|
-
### Health Checks
|
|
3819
|
-
|
|
3820
|
-
Expose standard health endpoints:
|
|
3821
|
-
|
|
3822
|
-
- `/health/live`: Is the process running? (Liveness)
|
|
3823
|
-
- `/health/ready`: Can it accept traffic? (Readiness)
|
|
3824
|
-
- `/health/startup`: Has it finished initializing? (Startup)
|
|
3825
|
-
|
|
3826
|
-
## Alerting Best Practices
|
|
3827
|
-
|
|
3828
|
-
- **Alert on Symptoms, not Causes**: "High Error Rate" (Symptom) vs "CPU High" (Cause).
|
|
3829
|
-
- **Golden Signals**: Latency, Traffic, Errors, Saturation.
|
|
3830
|
-
- **Actionable**: Every alert should require a specific human action.
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
---
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
## Best Practices
|
|
3837
|
-
|
|
3838
|
-
# Planning Best Practices
|
|
3839
|
-
|
|
3840
|
-
## Plan Before Implementation
|
|
3841
|
-
|
|
3842
|
-
**ALWAYS design and plan before writing code:**
|
|
3843
|
-
|
|
3844
|
-
1. **Understand Requirements**
|
|
3845
|
-
- Clarify the goal and scope
|
|
3846
|
-
- Identify constraints and dependencies
|
|
3847
|
-
- Ask questions about ambiguous requirements
|
|
3848
|
-
|
|
3849
|
-
2. **Break Down Into Phases**
|
|
3850
|
-
- Divide work into logical phases
|
|
3851
|
-
- Define deliverables for each phase
|
|
3852
|
-
- Prioritize phases by value and dependencies
|
|
3853
|
-
|
|
3854
|
-
3. **Design First**
|
|
3855
|
-
- Sketch architecture and data flow
|
|
3856
|
-
- Identify components and interfaces
|
|
3857
|
-
- Consider edge cases and error scenarios
|
|
3858
|
-
|
|
3859
|
-
4. **Get User Approval**
|
|
3860
|
-
- Present the plan to stakeholders
|
|
3861
|
-
- Explain trade-offs and alternatives
|
|
3862
|
-
- Wait for approval before implementation
|
|
3863
|
-
|
|
3864
|
-
## Never Make Assumptions
|
|
3865
|
-
|
|
3866
|
-
**CRITICAL: When in doubt, ASK:**
|
|
3867
|
-
|
|
3868
|
-
```typescript
|
|
3869
|
-
// ❌ BAD: Assuming what user wants
|
|
3870
|
-
async function processOrder(orderId: string) {
|
|
3871
|
-
// Assuming we should send email, but maybe not?
|
|
3872
|
-
await sendConfirmationEmail(orderId);
|
|
3873
|
-
// Assuming payment is already captured?
|
|
3874
|
-
await fulfillOrder(orderId);
|
|
3875
|
-
}
|
|
3876
|
-
|
|
3877
|
-
// ✅ GOOD: Clarify requirements first
|
|
3878
|
-
// Q: Should we send confirmation email at this stage?
|
|
3879
|
-
// Q: Is payment already captured or should we capture it here?
|
|
3880
|
-
// Q: What happens if fulfillment fails?
|
|
3881
|
-
```
|
|
3882
|
-
|
|
3883
|
-
**Ask about:**
|
|
3884
|
-
- Expected behavior in edge cases
|
|
3885
|
-
- Error handling strategy
|
|
3886
|
-
- Performance requirements
|
|
3887
|
-
- Security considerations
|
|
3888
|
-
- User experience preferences
|
|
3889
|
-
|
|
3890
|
-
## Plan in Phases
|
|
3891
|
-
|
|
3892
|
-
**Structure work into clear phases:**
|
|
3893
|
-
|
|
3894
|
-
### Phase 1: Foundation
|
|
3895
|
-
- Set up project structure
|
|
3896
|
-
- Configure tooling and dependencies
|
|
3897
|
-
- Create basic types and interfaces
|
|
3898
|
-
|
|
3899
|
-
### Phase 2: Core Implementation
|
|
3900
|
-
- Implement main business logic
|
|
3901
|
-
- Add error handling
|
|
3902
|
-
- Write unit tests
|
|
3903
|
-
|
|
3904
|
-
### Phase 3: Integration
|
|
3905
|
-
- Connect components
|
|
3906
|
-
- Add integration tests
|
|
3907
|
-
- Handle edge cases
|
|
3908
|
-
|
|
3909
|
-
### Phase 4: Polish
|
|
3910
|
-
- Performance optimization
|
|
3911
|
-
- Documentation
|
|
3912
|
-
- Final review
|
|
3913
|
-
|
|
3914
|
-
**Checkpoint after each phase:**
|
|
3915
|
-
- Demo functionality
|
|
3916
|
-
- Get feedback
|
|
3917
|
-
- Adjust plan if needed
|
|
3918
|
-
|
|
3919
|
-
## Planning Template
|
|
3920
|
-
|
|
3921
|
-
```markdown
|
|
3922
|
-
## Goal
|
|
3923
|
-
[What are we building and why?]
|
|
3924
|
-
|
|
3925
|
-
## Requirements
|
|
3926
|
-
- [ ] Requirement 1
|
|
3927
|
-
- [ ] Requirement 2
|
|
3928
|
-
- [ ] Requirement 3
|
|
3929
|
-
|
|
3930
|
-
## Questions for Clarification
|
|
3931
|
-
1. [Question about requirement X]
|
|
3932
|
-
2. [Question about edge case Y]
|
|
3933
|
-
3. [Question about preferred approach for Z]
|
|
3934
|
-
|
|
3935
|
-
## Proposed Approach
|
|
3936
|
-
[Describe the solution]
|
|
3937
|
-
|
|
3938
|
-
## Phases
|
|
3939
|
-
1. **Phase 1**: [Description]
|
|
3940
|
-
- Task 1
|
|
3941
|
-
- Task 2
|
|
3942
|
-
|
|
3943
|
-
2. **Phase 2**: [Description]
|
|
3944
|
-
- Task 1
|
|
3945
|
-
- Task 2
|
|
3946
|
-
|
|
3947
|
-
## Risks & Mitigation
|
|
3948
|
-
- **Risk**: [Description]
|
|
3949
|
-
**Mitigation**: [How to handle]
|
|
3950
|
-
|
|
3951
|
-
## Alternatives Considered
|
|
3952
|
-
- **Option A**: [Pros/Cons]
|
|
3953
|
-
- **Option B**: [Pros/Cons]
|
|
3954
|
-
- **Chosen**: Option A because [reason]
|
|
3955
|
-
```
|
|
3956
|
-
|
|
3957
|
-
## Communication Principles
|
|
3958
|
-
|
|
3959
|
-
1. **Ask Early**: Don't wait until you're stuck
|
|
3960
|
-
2. **Be Specific**: "Should error X retry or fail immediately?"
|
|
3961
|
-
3. **Propose Options**: "Would you prefer A or B?"
|
|
3962
|
-
4. **Explain Trade-offs**: "Fast but risky vs. Slow but safe"
|
|
3963
|
-
5. **Document Decisions**: Record what was decided and why
|
|
3964
|
-
|
|
3965
|
-
## Anti-Patterns
|
|
3966
|
-
|
|
3967
|
-
❌ **Don't:**
|
|
3968
|
-
- Start coding without understanding requirements
|
|
3969
|
-
- Assume you know what the user wants
|
|
3970
|
-
- Skip the planning phase to "save time"
|
|
3971
|
-
- Make architectural decisions without discussion
|
|
3972
|
-
- Proceed with unclear requirements
|
|
3973
|
-
|
|
3974
|
-
✅ **Do:**
|
|
3975
|
-
- Ask questions when requirements are vague
|
|
3976
|
-
- Create a plan and get it approved
|
|
3977
|
-
- Break work into reviewable phases
|
|
3978
|
-
- Document decisions and reasoning
|
|
3979
|
-
- Communicate early and often
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
# Documentation Organization
|
|
3983
|
-
|
|
3984
|
-
## Keep Root Clean
|
|
3985
|
-
|
|
3986
|
-
**RULE: Documentation must NOT clutter the project root.**
|
|
3987
|
-
|
|
3988
|
-
```
|
|
3989
|
-
❌ BAD: Root folder mess
|
|
3990
|
-
project/
|
|
3991
|
-
├── README.md
|
|
3992
|
-
├── ARCHITECTURE.md
|
|
3993
|
-
├── API_DOCS.md
|
|
3994
|
-
├── DEPLOYMENT.md
|
|
3995
|
-
├── TROUBLESHOOTING.md
|
|
3996
|
-
├── USER_GUIDE.md
|
|
3997
|
-
├── DATABASE_SCHEMA.md
|
|
3998
|
-
├── TESTING_GUIDE.md
|
|
3999
|
-
└── ... (20 more .md files)
|
|
4000
|
-
|
|
4001
|
-
✅ GOOD: Organized structure
|
|
4002
|
-
project/
|
|
4003
|
-
├── README.md (overview only)
|
|
4004
|
-
├── docs/
|
|
4005
|
-
│ ├── architecture/
|
|
4006
|
-
│ ├── api/
|
|
4007
|
-
│ ├── deployment/
|
|
4008
|
-
│ └── guides/
|
|
4009
|
-
└── src/
|
|
4010
|
-
```
|
|
4011
|
-
|
|
4012
|
-
## Documentation Structure
|
|
4013
|
-
|
|
4014
|
-
**Standard documentation folder:**
|
|
4015
|
-
|
|
4016
|
-
```
|
|
4017
|
-
docs/
|
|
4018
|
-
├── architecture/
|
|
4019
|
-
│ ├── overview.md
|
|
4020
|
-
│ ├── decisions/ # Architecture Decision Records
|
|
4021
|
-
│ │ ├── 001-database-choice.md
|
|
4022
|
-
│ │ └── 002-api-design.md
|
|
4023
|
-
│ └── diagrams/
|
|
4024
|
-
│
|
|
4025
|
-
├── api/
|
|
4026
|
-
│ ├── endpoints.md
|
|
4027
|
-
│ ├── authentication.md
|
|
4028
|
-
│ └── examples/
|
|
4029
|
-
│
|
|
4030
|
-
├── guides/
|
|
4031
|
-
│ ├── getting-started.md
|
|
4032
|
-
│ ├── development.md
|
|
4033
|
-
│ ├── deployment.md
|
|
4034
|
-
│ └── troubleshooting.md
|
|
4035
|
-
│
|
|
4036
|
-
├── features/ # Organize by feature
|
|
4037
|
-
│ ├── user-auth/
|
|
4038
|
-
│ │ ├── overview.md
|
|
4039
|
-
│ │ ├── implementation.md
|
|
4040
|
-
│ │ └── testing.md
|
|
4041
|
-
│ ├── payments/
|
|
4042
|
-
│ └── notifications/
|
|
4043
|
-
│
|
|
4044
|
-
└── planning/ # Active work planning
|
|
4045
|
-
├── memory-lane.md # Context preservation
|
|
4046
|
-
├── current-phase.md # Active work
|
|
4047
|
-
└── next-steps.md # Backlog
|
|
4048
|
-
```
|
|
4049
|
-
|
|
4050
|
-
## Memory Lane Document
|
|
4051
|
-
|
|
4052
|
-
**CRITICAL: Maintain context across sessions**
|
|
4053
|
-
|
|
4054
|
-
### Purpose
|
|
4055
|
-
When AI context limit is reached, reload from memory lane to restore working context.
|
|
4056
|
-
|
|
4057
|
-
### Structure
|
|
4058
|
-
|
|
4059
|
-
```markdown
|
|
4060
|
-
# Memory Lane - Project Context
|
|
4061
|
-
|
|
4062
|
-
## Last Updated
|
|
4063
|
-
2024-12-10 15:30
|
|
4064
|
-
|
|
4065
|
-
## Current Objective
|
|
4066
|
-
Implementing user authentication system with OAuth2 support
|
|
4067
|
-
|
|
4068
|
-
## Recent Progress
|
|
4069
|
-
- ✅ Set up database schema (2024-12-08)
|
|
4070
|
-
- ✅ Implemented user registration (2024-12-09)
|
|
4071
|
-
- 🔄 Working on: OAuth2 integration (2024-12-10)
|
|
4072
|
-
- ⏳ Next: Session management
|
|
4073
|
-
|
|
4074
|
-
## Key Decisions
|
|
4075
|
-
1. **Database**: PostgreSQL chosen for ACID compliance
|
|
4076
|
-
2. **Auth Strategy**: OAuth2 + JWT tokens
|
|
4077
|
-
3. **Session Store**: Redis for performance
|
|
4078
|
-
|
|
4079
|
-
## Important Files
|
|
4080
|
-
- `src/auth/oauth.ts` - OAuth2 implementation (IN PROGRESS)
|
|
4081
|
-
- `src/models/user.ts` - User model and validation
|
|
4082
|
-
- `docs/architecture/decisions/003-auth-system.md` - Full context
|
|
4083
|
-
|
|
4084
|
-
## Active Questions
|
|
4085
|
-
1. Should we support refresh tokens? (Pending user decision)
|
|
4086
|
-
2. Token expiry: 1h or 24h? (Pending user decision)
|
|
4087
|
-
|
|
4088
|
-
## Technical Context
|
|
4089
|
-
- Using Passport.js for OAuth
|
|
4090
|
-
- Google and GitHub providers configured
|
|
4091
|
-
- Callback URLs: /auth/google/callback, /auth/github/callback
|
|
4092
|
-
|
|
4093
|
-
## Known Issues
|
|
4094
|
-
- OAuth redirect not working in development (investigating)
|
|
4095
|
-
- Need to add rate limiting to prevent abuse
|
|
4096
|
-
|
|
4097
|
-
## Next Session
|
|
4098
|
-
1. Fix OAuth redirect issue
|
|
4099
|
-
2. Implement refresh token rotation
|
|
4100
|
-
3. Add comprehensive auth tests
|
|
4101
|
-
```
|
|
4102
|
-
|
|
4103
|
-
### Update Frequency
|
|
4104
|
-
- Update after each significant milestone
|
|
4105
|
-
- Update before context limit is reached
|
|
4106
|
-
- Update when switching between features
|
|
4107
|
-
|
|
4108
|
-
## Context Reload Strategy
|
|
4109
|
-
|
|
4110
|
-
**For AI Tools with Hooks:**
|
|
4111
|
-
|
|
4112
|
-
Create a hook to reload memory lane on startup:
|
|
4113
|
-
|
|
4114
|
-
```json
|
|
4115
|
-
{
|
|
4116
|
-
"hooks": {
|
|
4117
|
-
"startup": {
|
|
4118
|
-
"command": "cat docs/planning/memory-lane.md"
|
|
4119
|
-
}
|
|
4120
|
-
}
|
|
4121
|
-
}
|
|
4122
|
-
```
|
|
4123
|
-
|
|
4124
|
-
**For AI Tools with Agents:**
|
|
4125
|
-
|
|
4126
|
-
Create a context restoration agent:
|
|
4127
|
-
|
|
4128
|
-
```markdown
|
|
4129
|
-
# Context Restoration Agent
|
|
4130
|
-
|
|
4131
|
-
Task: Read and summarize current project state
|
|
4132
|
-
|
|
4133
|
-
Sources:
|
|
4134
|
-
1. docs/planning/memory-lane.md
|
|
4135
|
-
2. docs/architecture/decisions/ (recent ADRs)
|
|
4136
|
-
3. git log --oneline -10 (recent commits)
|
|
4137
|
-
|
|
4138
|
-
Output: Concise summary of where we are and what's next
|
|
4139
|
-
```
|
|
4140
|
-
|
|
4141
|
-
## Feature Documentation
|
|
4142
|
-
|
|
4143
|
-
**Organize by feature/scope, not by type:**
|
|
4144
|
-
|
|
4145
|
-
```
|
|
4146
|
-
❌ BAD: Organized by document type
|
|
4147
|
-
docs/
|
|
4148
|
-
├── specifications/
|
|
4149
|
-
│ ├── auth.md
|
|
4150
|
-
│ ├── payments.md
|
|
4151
|
-
│ └── notifications.md
|
|
4152
|
-
├── implementations/
|
|
4153
|
-
│ ├── auth.md
|
|
4154
|
-
│ ├── payments.md
|
|
4155
|
-
│ └── notifications.md
|
|
4156
|
-
└── tests/
|
|
4157
|
-
├── auth.md
|
|
4158
|
-
└── payments.md
|
|
4159
|
-
|
|
4160
|
-
✅ GOOD: Organized by feature
|
|
4161
|
-
docs/features/
|
|
4162
|
-
├── auth/
|
|
4163
|
-
│ ├── specification.md
|
|
4164
|
-
│ ├── implementation.md
|
|
4165
|
-
│ ├── api.md
|
|
4166
|
-
│ └── testing.md
|
|
4167
|
-
├── payments/
|
|
4168
|
-
│ ├── specification.md
|
|
4169
|
-
│ ├── implementation.md
|
|
4170
|
-
│ └── providers.md
|
|
4171
|
-
└── notifications/
|
|
4172
|
-
├── specification.md
|
|
4173
|
-
└── channels.md
|
|
4174
|
-
```
|
|
4175
|
-
|
|
4176
|
-
**Benefits:**
|
|
4177
|
-
- All related docs in one place
|
|
4178
|
-
- Easy to find feature-specific information
|
|
4179
|
-
- Natural scope boundaries
|
|
4180
|
-
- Easier to maintain
|
|
4181
|
-
|
|
4182
|
-
## Planning Documents
|
|
4183
|
-
|
|
4184
|
-
**Active planning should be in docs/planning/:**
|
|
4185
|
-
|
|
4186
|
-
```
|
|
4187
|
-
docs/planning/
|
|
4188
|
-
├── memory-lane.md # Context preservation
|
|
4189
|
-
├── current-sprint.md # Active work
|
|
4190
|
-
├── backlog.md # Future work
|
|
4191
|
-
└── spike-results/ # Research findings
|
|
4192
|
-
├── database-options.md
|
|
4193
|
-
└── auth-libraries.md
|
|
4194
|
-
```
|
|
4195
|
-
|
|
4196
|
-
## Documentation Principles
|
|
4197
|
-
|
|
4198
|
-
1. **Separate folder**: All docs in `docs/` directory
|
|
4199
|
-
2. **Organize by scope**: Group by feature, not document type
|
|
4200
|
-
3. **Keep root clean**: Only README.md in project root
|
|
4201
|
-
4. **Maintain memory lane**: Update regularly for context preservation
|
|
4202
|
-
5. **Link related docs**: Use relative links between related documents
|
|
4203
|
-
|
|
4204
|
-
## README Guidelines
|
|
4205
|
-
|
|
4206
|
-
**Root README should be concise:**
|
|
4207
|
-
|
|
4208
|
-
```markdown
|
|
4209
|
-
# Project Name
|
|
4210
|
-
|
|
4211
|
-
Brief description
|
|
4212
|
-
|
|
4213
|
-
## Quick Start
|
|
4214
|
-
[Link to docs/guides/getting-started.md]
|
|
4215
|
-
|
|
4216
|
-
## Documentation
|
|
4217
|
-
- [Architecture](docs/architecture/overview.md)
|
|
4218
|
-
- [API Docs](docs/api/endpoints.md)
|
|
4219
|
-
- [Development Guide](docs/guides/development.md)
|
|
4220
|
-
|
|
4221
|
-
## Contributing
|
|
4222
|
-
[Link to CONTRIBUTING.md or docs/guides/contributing.md]
|
|
4223
|
-
```
|
|
4224
|
-
|
|
4225
|
-
**Keep it short, link to detailed docs.**
|
|
4226
|
-
|
|
4227
|
-
## Anti-Patterns
|
|
4228
|
-
|
|
4229
|
-
❌ **Don't:**
|
|
4230
|
-
- Put 10+ markdown files in project root
|
|
4231
|
-
- Mix documentation types in same folder
|
|
4232
|
-
- Forget to update memory lane before context expires
|
|
4233
|
-
- Create documentation without clear organization
|
|
4234
|
-
- Duplicate information across multiple docs
|
|
4235
|
-
|
|
4236
|
-
✅ **Do:**
|
|
4237
|
-
- Use `docs/` directory for all documentation
|
|
4238
|
-
- Organize by feature/scope
|
|
4239
|
-
- Maintain memory lane for context preservation
|
|
4240
|
-
- Link related documents together
|
|
4241
|
-
- Update docs as code evolves
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
# Code Review Practices
|
|
4245
|
-
|
|
4246
|
-
## Review Checklist
|
|
4247
|
-
|
|
4248
|
-
- [ ] Code follows project style guidelines
|
|
4249
|
-
- [ ] No obvious bugs or logic errors
|
|
4250
|
-
- [ ] Error handling is appropriate
|
|
4251
|
-
- [ ] Tests cover new functionality
|
|
4252
|
-
- [ ] No security vulnerabilities introduced
|
|
4253
|
-
- [ ] Performance implications considered
|
|
4254
|
-
- [ ] Documentation updated if needed
|
|
4255
|
-
|
|
4256
|
-
## Giving Feedback
|
|
4257
|
-
|
|
4258
|
-
**Good:**
|
|
4259
|
-
```
|
|
4260
|
-
Consider using `Array.find()` here instead of `filter()[0]` -
|
|
4261
|
-
it's more readable and stops at the first match.
|
|
4262
|
-
```
|
|
4263
|
-
|
|
4264
|
-
**Bad:**
|
|
4265
|
-
```
|
|
4266
|
-
This is wrong.
|
|
4267
|
-
```
|
|
4268
|
-
|
|
4269
|
-
## PR Description Template
|
|
4270
|
-
|
|
4271
|
-
```markdown
|
|
4272
|
-
## Summary
|
|
4273
|
-
Brief description of changes
|
|
4274
|
-
|
|
4275
|
-
## Changes
|
|
4276
|
-
- Added X feature
|
|
4277
|
-
- Fixed Y bug
|
|
4278
|
-
- Refactored Z
|
|
4279
|
-
|
|
4280
|
-
## Testing
|
|
4281
|
-
- [ ] Unit tests added
|
|
4282
|
-
- [ ] Manual testing performed
|
|
4283
|
-
|
|
4284
|
-
## Screenshots (if UI changes)
|
|
4285
|
-
```
|
|
4286
|
-
|
|
4287
|
-
## Best Practices
|
|
4288
|
-
|
|
4289
|
-
- Review promptly (within 24 hours)
|
|
4290
|
-
- Focus on logic and design, not style (use linters)
|
|
4291
|
-
- Ask questions rather than make demands
|
|
4292
|
-
- Praise good solutions
|
|
4293
|
-
- Keep PRs small and focused
|
|
4294
|
-
- Use "nitpick:" prefix for minor suggestions
|
|
4295
|
-
- Approve with minor comments when appropriate
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
# Refactoring Patterns
|
|
4299
|
-
|
|
4300
|
-
## Common Code Smells
|
|
4301
|
-
|
|
4302
|
-
### Long Method
|
|
4303
|
-
Split into smaller, focused functions.
|
|
4304
|
-
|
|
4305
|
-
```typescript
|
|
4306
|
-
// Before
|
|
4307
|
-
function processOrder(order: Order) {
|
|
4308
|
-
// 100 lines of code...
|
|
4309
|
-
}
|
|
4310
|
-
|
|
4311
|
-
// After
|
|
4312
|
-
function processOrder(order: Order) {
|
|
4313
|
-
validateOrder(order);
|
|
4314
|
-
calculateTotals(order);
|
|
4315
|
-
applyDiscounts(order);
|
|
4316
|
-
saveOrder(order);
|
|
4317
|
-
}
|
|
4318
|
-
```
|
|
4319
|
-
|
|
4320
|
-
### Duplicate Code
|
|
4321
|
-
Extract common logic.
|
|
4322
|
-
|
|
4323
|
-
```typescript
|
|
4324
|
-
// Before
|
|
4325
|
-
function getAdminUsers() {
|
|
4326
|
-
return users.filter(u => u.role === 'admin' && u.active);
|
|
4327
|
-
}
|
|
4328
|
-
function getModeratorUsers() {
|
|
4329
|
-
return users.filter(u => u.role === 'moderator' && u.active);
|
|
4330
|
-
}
|
|
4331
|
-
|
|
4332
|
-
// After
|
|
4333
|
-
function getActiveUsersByRole(role: string) {
|
|
4334
|
-
return users.filter(u => u.role === role && u.active);
|
|
4335
|
-
}
|
|
4336
|
-
```
|
|
4337
|
-
|
|
4338
|
-
### Primitive Obsession
|
|
4339
|
-
Use value objects.
|
|
4340
|
-
|
|
4341
|
-
```typescript
|
|
4342
|
-
// Before
|
|
4343
|
-
function sendEmail(email: string) { /* ... */ }
|
|
4344
|
-
|
|
4345
|
-
// After
|
|
4346
|
-
class Email {
|
|
4347
|
-
constructor(private value: string) {
|
|
4348
|
-
if (!this.isValid(value)) throw new Error('Invalid email');
|
|
4349
|
-
}
|
|
4350
|
-
}
|
|
4351
|
-
function sendEmail(email: Email) { /* ... */ }
|
|
4352
|
-
```
|
|
4353
|
-
|
|
4354
|
-
### Feature Envy
|
|
4355
|
-
Move method to class it uses most.
|
|
4356
|
-
|
|
4357
|
-
```typescript
|
|
4358
|
-
// Before - Order is accessing customer too much
|
|
4359
|
-
class Order {
|
|
4360
|
-
getDiscount() {
|
|
4361
|
-
return this.customer.isPremium() ?
|
|
4362
|
-
this.customer.premiumDiscount :
|
|
4363
|
-
this.customer.regularDiscount;
|
|
4364
|
-
}
|
|
4365
|
-
}
|
|
4366
|
-
|
|
4367
|
-
// After
|
|
4368
|
-
class Customer {
|
|
4369
|
-
getDiscount(): number {
|
|
4370
|
-
return this.isPremium() ? this.premiumDiscount : this.regularDiscount;
|
|
4371
|
-
}
|
|
4372
|
-
}
|
|
4373
|
-
```
|
|
4374
|
-
|
|
4375
|
-
## Safe Refactoring Steps
|
|
4376
|
-
|
|
4377
|
-
1. Ensure tests pass before refactoring
|
|
4378
|
-
2. Make one small change at a time
|
|
4379
|
-
3. Run tests after each change
|
|
4380
|
-
4. Commit frequently
|
|
4381
|
-
5. Refactor in separate commits from feature work
|
|
4382
|
-
|
|
4383
|
-
## Best Practices
|
|
4384
|
-
|
|
4385
|
-
- Refactor when adding features, not separately
|
|
4386
|
-
- Keep refactoring commits separate
|
|
4387
|
-
- Use IDE refactoring tools when available
|
|
4388
|
-
- Write tests before refactoring if missing
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
# Version Control Patterns
|
|
4392
|
-
|
|
4393
|
-
## Branching Strategies
|
|
4394
|
-
|
|
4395
|
-
### GitHub Flow
|
|
4396
|
-
Simple: main + feature branches.
|
|
4397
|
-
|
|
4398
|
-
```
|
|
4399
|
-
main ─────●─────●─────●─────●─────
|
|
4400
|
-
\ /
|
|
4401
|
-
feature ●───●───●
|
|
4402
|
-
```
|
|
4403
|
-
|
|
4404
|
-
### Git Flow
|
|
4405
|
-
For scheduled releases: main, develop, feature, release, hotfix.
|
|
4406
|
-
|
|
4407
|
-
```
|
|
4408
|
-
main ─────●─────────────●─────
|
|
4409
|
-
\ /
|
|
4410
|
-
release ●─────────●
|
|
4411
|
-
\ /
|
|
4412
|
-
develop ●───●───●───●───●───●───
|
|
4413
|
-
\ /
|
|
4414
|
-
feature ●───●
|
|
4415
|
-
```
|
|
4416
|
-
|
|
4417
|
-
## Commit Messages
|
|
4418
|
-
|
|
4419
|
-
```
|
|
4420
|
-
feat: add user authentication
|
|
4421
|
-
|
|
4422
|
-
- Implement JWT-based auth
|
|
4423
|
-
- Add login/logout endpoints
|
|
4424
|
-
- Include password hashing
|
|
4425
|
-
|
|
4426
|
-
Closes #123
|
|
4427
|
-
```
|
|
4428
|
-
|
|
4429
|
-
**Prefixes:**
|
|
4430
|
-
- `feat:` - New feature
|
|
4431
|
-
- `fix:` - Bug fix
|
|
4432
|
-
- `refactor:` - Code change that doesn't fix bug or add feature
|
|
4433
|
-
- `docs:` - Documentation only
|
|
4434
|
-
- `test:` - Adding tests
|
|
4435
|
-
- `chore:` - Maintenance tasks
|
|
4436
|
-
|
|
4437
|
-
## Best Practices
|
|
4438
|
-
|
|
4439
|
-
- Keep commits atomic and focused
|
|
4440
|
-
- Write descriptive commit messages
|
|
4441
|
-
- Pull/rebase before pushing
|
|
4442
|
-
- Never force push to shared branches
|
|
4443
|
-
- Use pull requests for code review
|
|
4444
|
-
- Delete merged branches
|
|
4445
|
-
- Tag releases with semantic versions
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
---
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
## Design Patterns
|
|
4452
|
-
|
|
4453
|
-
# Base Patterns
|
|
4454
|
-
|
|
4455
|
-
## Value Object
|
|
4456
|
-
|
|
4457
|
-
Immutable object defined by its value, not identity.
|
|
4458
|
-
|
|
4459
|
-
```typescript
|
|
4460
|
-
class Email {
|
|
4461
|
-
private readonly value: string;
|
|
4462
|
-
|
|
4463
|
-
constructor(email: string) {
|
|
4464
|
-
if (!this.isValid(email)) throw new Error('Invalid email');
|
|
4465
|
-
this.value = email.toLowerCase();
|
|
4466
|
-
}
|
|
4467
|
-
|
|
4468
|
-
equals(other: Email): boolean {
|
|
4469
|
-
return this.value === other.value;
|
|
4470
|
-
}
|
|
4471
|
-
}
|
|
4472
|
-
|
|
4473
|
-
class Money {
|
|
4474
|
-
constructor(
|
|
4475
|
-
public readonly amount: number,
|
|
4476
|
-
public readonly currency: Currency
|
|
4477
|
-
) {
|
|
4478
|
-
Object.freeze(this);
|
|
4479
|
-
}
|
|
4480
|
-
|
|
4481
|
-
add(other: Money): Money {
|
|
4482
|
-
this.assertSameCurrency(other);
|
|
4483
|
-
return new Money(this.amount + other.amount, this.currency);
|
|
4484
|
-
}
|
|
4485
|
-
}
|
|
4486
|
-
```
|
|
4487
|
-
|
|
4488
|
-
## Special Case (Null Object)
|
|
4489
|
-
|
|
4490
|
-
Replace null checks with polymorphism.
|
|
4491
|
-
|
|
4492
|
-
```typescript
|
|
4493
|
-
abstract class Customer {
|
|
4494
|
-
abstract getDiscount(): number;
|
|
4495
|
-
}
|
|
4496
|
-
|
|
4497
|
-
class RealCustomer extends Customer {
|
|
4498
|
-
getDiscount(): number { return 0.1; }
|
|
4499
|
-
}
|
|
4500
|
-
|
|
4501
|
-
class GuestCustomer extends Customer {
|
|
4502
|
-
getDiscount(): number { return 0; } // No discount
|
|
4503
|
-
}
|
|
4504
|
-
|
|
4505
|
-
// No null checks needed
|
|
4506
|
-
const customer = repo.findById(id) || new GuestCustomer();
|
|
4507
|
-
const discount = customer.getDiscount();
|
|
4508
|
-
```
|
|
4509
|
-
|
|
4510
|
-
## Registry
|
|
4511
|
-
|
|
4512
|
-
Global access point for services.
|
|
4513
|
-
|
|
4514
|
-
```typescript
|
|
4515
|
-
class ServiceRegistry {
|
|
4516
|
-
private static services = new Map<string, any>();
|
|
4517
|
-
|
|
4518
|
-
static register<T>(key: string, service: T): void {
|
|
4519
|
-
this.services.set(key, service);
|
|
4520
|
-
}
|
|
4521
|
-
|
|
4522
|
-
static get<T>(key: string): T {
|
|
4523
|
-
return this.services.get(key);
|
|
4524
|
-
}
|
|
4525
|
-
}
|
|
4526
|
-
|
|
4527
|
-
// Prefer dependency injection over registry
|
|
4528
|
-
```
|
|
4529
|
-
|
|
4530
|
-
## Plugin
|
|
4531
|
-
|
|
4532
|
-
Extend behavior without modifying core code.
|
|
4533
|
-
|
|
4534
|
-
```typescript
|
|
4535
|
-
interface ValidationPlugin {
|
|
4536
|
-
validate(user: User): ValidationResult;
|
|
4537
|
-
}
|
|
4538
|
-
|
|
4539
|
-
class UserValidator {
|
|
4540
|
-
private plugins: ValidationPlugin[] = [];
|
|
4541
|
-
|
|
4542
|
-
registerPlugin(plugin: ValidationPlugin): void {
|
|
4543
|
-
this.plugins.push(plugin);
|
|
4544
|
-
}
|
|
4545
|
-
|
|
4546
|
-
validate(user: User): ValidationResult[] {
|
|
4547
|
-
return this.plugins.map(p => p.validate(user));
|
|
4548
|
-
}
|
|
4549
|
-
}
|
|
4550
|
-
```
|
|
4551
|
-
|
|
4552
|
-
## Best Practices
|
|
4553
|
-
|
|
4554
|
-
- Use Value Objects to avoid primitive obsession
|
|
4555
|
-
- Make Value Objects immutable
|
|
4556
|
-
- Use Special Case instead of null checks
|
|
4557
|
-
- Prefer dependency injection over Registry
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
# Data Access Patterns
|
|
4561
|
-
|
|
4562
|
-
## Repository
|
|
4563
|
-
|
|
4564
|
-
Collection-like interface for domain objects.
|
|
4565
|
-
|
|
4566
|
-
```typescript
|
|
4567
|
-
interface UserRepository {
|
|
4568
|
-
findById(id: string): Promise<User | null>;
|
|
4569
|
-
findByEmail(email: string): Promise<User | null>;
|
|
4570
|
-
save(user: User): Promise<void>;
|
|
4571
|
-
delete(user: User): Promise<void>;
|
|
4572
|
-
}
|
|
4573
|
-
|
|
4574
|
-
class PostgreSQLUserRepository implements UserRepository {
|
|
4575
|
-
async findById(id: string): Promise<User | null> {
|
|
4576
|
-
const row = await this.db.queryOne('SELECT * FROM users WHERE id = $1', [id]);
|
|
4577
|
-
return row ? this.mapToUser(row) : null;
|
|
4578
|
-
}
|
|
4579
|
-
}
|
|
4580
|
-
```
|
|
4581
|
-
|
|
4582
|
-
## Data Mapper
|
|
4583
|
-
|
|
4584
|
-
Complete separation between domain and persistence.
|
|
4585
|
-
|
|
4586
|
-
```typescript
|
|
4587
|
-
class UserMapper {
|
|
4588
|
-
toDomain(row: DbRow): User {
|
|
4589
|
-
return new User(row.id, row.name, new Email(row.email));
|
|
4590
|
-
}
|
|
4591
|
-
|
|
4592
|
-
toDatabase(user: User): DbRow {
|
|
4593
|
-
return { id: user.id, name: user.name, email: user.email.toString() };
|
|
4594
|
-
}
|
|
4595
|
-
}
|
|
4596
|
-
```
|
|
4597
|
-
|
|
4598
|
-
## Unit of Work
|
|
4599
|
-
|
|
4600
|
-
Track changes and commit together.
|
|
4601
|
-
|
|
4602
|
-
```typescript
|
|
4603
|
-
class UnitOfWork {
|
|
4604
|
-
private newObjects = new Set<any>();
|
|
4605
|
-
private dirtyObjects = new Set<any>();
|
|
4606
|
-
|
|
4607
|
-
registerNew(obj: any): void { this.newObjects.add(obj); }
|
|
4608
|
-
registerDirty(obj: any): void { this.dirtyObjects.add(obj); }
|
|
4609
|
-
|
|
4610
|
-
async commit(): Promise<void> {
|
|
4611
|
-
await this.db.beginTransaction();
|
|
4612
|
-
try {
|
|
4613
|
-
for (const obj of this.newObjects) await this.insert(obj);
|
|
4614
|
-
for (const obj of this.dirtyObjects) await this.update(obj);
|
|
4615
|
-
await this.db.commit();
|
|
4616
|
-
} catch (e) {
|
|
4617
|
-
await this.db.rollback();
|
|
4618
|
-
throw e;
|
|
4619
|
-
}
|
|
4620
|
-
}
|
|
4621
|
-
}
|
|
4622
|
-
```
|
|
4623
|
-
|
|
4624
|
-
## Identity Map
|
|
4625
|
-
|
|
4626
|
-
Ensure each object loaded only once per session.
|
|
4627
|
-
|
|
4628
|
-
```typescript
|
|
4629
|
-
class IdentityMap {
|
|
4630
|
-
private map = new Map<string, any>();
|
|
4631
|
-
|
|
4632
|
-
get(id: string): any | null { return this.map.get(id) || null; }
|
|
4633
|
-
put(id: string, obj: any): void { this.map.set(id, obj); }
|
|
4634
|
-
}
|
|
4635
|
-
```
|
|
4636
|
-
|
|
4637
|
-
## Best Practices
|
|
4638
|
-
|
|
4639
|
-
- Return domain objects from repositories
|
|
4640
|
-
- Use one repository per aggregate root
|
|
4641
|
-
- Keep repositories focused on persistence
|
|
4642
|
-
- Don't leak database details into domain
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
# Domain Logic Patterns
|
|
4646
|
-
|
|
4647
|
-
## Transaction Script
|
|
4648
|
-
|
|
4649
|
-
Procedural approach - one procedure per operation.
|
|
4650
|
-
|
|
4651
|
-
```typescript
|
|
4652
|
-
async function transferMoney(fromId: string, toId: string, amount: number) {
|
|
4653
|
-
const db = await Database.connect();
|
|
4654
|
-
await db.beginTransaction();
|
|
4655
|
-
|
|
4656
|
-
try {
|
|
4657
|
-
const from = await db.query('SELECT * FROM accounts WHERE id = $1', [fromId]);
|
|
4658
|
-
if (from.balance < amount) throw new Error('Insufficient funds');
|
|
4659
|
-
|
|
4660
|
-
await db.execute('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId]);
|
|
4661
|
-
await db.execute('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId]);
|
|
4662
|
-
await db.commit();
|
|
4663
|
-
} catch (e) {
|
|
4664
|
-
await db.rollback();
|
|
4665
|
-
throw e;
|
|
4666
|
-
}
|
|
4667
|
-
}
|
|
4668
|
-
```
|
|
4669
|
-
|
|
4670
|
-
**Use for:** Simple apps, CRUD, reports.
|
|
4671
|
-
|
|
4672
|
-
## Domain Model
|
|
4673
|
-
|
|
4674
|
-
Rich objects with behavior.
|
|
4675
|
-
|
|
4676
|
-
```typescript
|
|
4677
|
-
class Account {
|
|
4678
|
-
constructor(private balance: Money, private overdraftLimit: Money) {}
|
|
4679
|
-
|
|
4680
|
-
withdraw(amount: Money): void {
|
|
4681
|
-
if (!this.canWithdraw(amount)) throw new InsufficientFundsError();
|
|
4682
|
-
this.balance = this.balance.subtract(amount);
|
|
4683
|
-
}
|
|
4684
|
-
|
|
4685
|
-
transfer(amount: Money, recipient: Account): void {
|
|
4686
|
-
this.withdraw(amount);
|
|
4687
|
-
recipient.deposit(amount);
|
|
4688
|
-
}
|
|
4689
|
-
}
|
|
4690
|
-
```
|
|
4691
|
-
|
|
4692
|
-
**Use for:** Complex business rules, rich domains.
|
|
4693
|
-
|
|
4694
|
-
## Service Layer
|
|
4695
|
-
|
|
4696
|
-
Application boundary coordinating domain objects.
|
|
4697
|
-
|
|
4698
|
-
```typescript
|
|
4699
|
-
class AccountService {
|
|
4700
|
-
constructor(
|
|
4701
|
-
private accountRepo: AccountRepository,
|
|
4702
|
-
private unitOfWork: UnitOfWork
|
|
4703
|
-
) {}
|
|
4704
|
-
|
|
4705
|
-
async transfer(fromId: string, toId: string, amount: Money): Promise<void> {
|
|
4706
|
-
const from = await this.accountRepo.findById(fromId);
|
|
4707
|
-
const to = await this.accountRepo.findById(toId);
|
|
4708
|
-
|
|
4709
|
-
from.transfer(amount, to); // Domain logic
|
|
4710
|
-
|
|
4711
|
-
await this.accountRepo.save(from);
|
|
4712
|
-
await this.accountRepo.save(to);
|
|
4713
|
-
await this.unitOfWork.commit();
|
|
4714
|
-
}
|
|
4715
|
-
}
|
|
4716
|
-
```
|
|
4717
|
-
|
|
4718
|
-
**Use for:** API boundaries, multiple clients, transaction coordination.
|
|
4719
|
-
|
|
4720
|
-
## Best Practices
|
|
4721
|
-
|
|
4722
|
-
- Choose pattern based on complexity
|
|
4723
|
-
- Service layer orchestrates, domain model contains logic
|
|
4724
|
-
- Keep services thin, domain objects rich
|
|
4725
|
-
- Combine Domain Model + Service Layer for complex apps
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
# Gang of Four Patterns
|
|
4729
|
-
|
|
4730
|
-
## Creational
|
|
4731
|
-
|
|
4732
|
-
### Factory Method
|
|
4733
|
-
```typescript
|
|
4734
|
-
interface Logger { log(msg: string): void; }
|
|
4735
|
-
|
|
4736
|
-
abstract class Application {
|
|
4737
|
-
abstract createLogger(): Logger;
|
|
4738
|
-
run(): void { this.createLogger().log('Started'); }
|
|
4739
|
-
}
|
|
4740
|
-
|
|
4741
|
-
class DevApp extends Application {
|
|
4742
|
-
createLogger(): Logger { return new ConsoleLogger(); }
|
|
4743
|
-
}
|
|
4744
|
-
```
|
|
4745
|
-
|
|
4746
|
-
### Builder
|
|
4747
|
-
```typescript
|
|
4748
|
-
const query = new QueryBuilder()
|
|
4749
|
-
.from('users')
|
|
4750
|
-
.select('id', 'name')
|
|
4751
|
-
.where('active = true')
|
|
4752
|
-
.limit(10)
|
|
4753
|
-
.build();
|
|
4754
|
-
```
|
|
4755
|
-
|
|
4756
|
-
## Structural
|
|
4757
|
-
|
|
4758
|
-
### Adapter
|
|
4759
|
-
```typescript
|
|
4760
|
-
class PaymentAdapter implements PaymentProcessor {
|
|
4761
|
-
constructor(private legacy: OldPaymentSystem) {}
|
|
4762
|
-
|
|
4763
|
-
async process(amount: number): Promise<boolean> {
|
|
4764
|
-
return this.legacy.makePayment(amount);
|
|
4765
|
-
}
|
|
4766
|
-
}
|
|
4767
|
-
```
|
|
4768
|
-
|
|
4769
|
-
### Decorator
|
|
4770
|
-
```typescript
|
|
4771
|
-
interface Coffee { cost(): number; }
|
|
4772
|
-
|
|
4773
|
-
class MilkDecorator implements Coffee {
|
|
4774
|
-
constructor(private coffee: Coffee) {}
|
|
4775
|
-
cost(): number { return this.coffee.cost() + 2; }
|
|
4776
|
-
}
|
|
4777
|
-
|
|
4778
|
-
let coffee: Coffee = new SimpleCoffee();
|
|
4779
|
-
coffee = new MilkDecorator(coffee);
|
|
4780
|
-
```
|
|
4781
|
-
|
|
4782
|
-
### Facade
|
|
4783
|
-
```typescript
|
|
4784
|
-
class ComputerFacade {
|
|
4785
|
-
start(): void {
|
|
4786
|
-
this.cpu.freeze();
|
|
4787
|
-
this.memory.load(0, this.hdd.read(0, 1024));
|
|
4788
|
-
this.cpu.execute();
|
|
4789
|
-
}
|
|
4790
|
-
}
|
|
4791
|
-
```
|
|
4792
|
-
|
|
4793
|
-
## Behavioral
|
|
4794
|
-
|
|
4795
|
-
### Strategy
|
|
4796
|
-
```typescript
|
|
4797
|
-
interface SortStrategy { sort(data: number[]): number[]; }
|
|
4798
|
-
|
|
4799
|
-
class Sorter {
|
|
4800
|
-
constructor(private strategy: SortStrategy) {}
|
|
4801
|
-
sort(data: number[]): number[] { return this.strategy.sort(data); }
|
|
4802
|
-
}
|
|
4803
|
-
```
|
|
4804
|
-
|
|
4805
|
-
### Observer
|
|
4806
|
-
```typescript
|
|
4807
|
-
class Stock {
|
|
4808
|
-
private observers: Observer[] = [];
|
|
4809
|
-
|
|
4810
|
-
attach(o: Observer): void { this.observers.push(o); }
|
|
4811
|
-
notify(): void { this.observers.forEach(o => o.update(this)); }
|
|
4812
|
-
|
|
4813
|
-
setPrice(price: number): void {
|
|
4814
|
-
this.price = price;
|
|
4815
|
-
this.notify();
|
|
4816
|
-
}
|
|
4817
|
-
}
|
|
4818
|
-
```
|
|
4819
|
-
|
|
4820
|
-
### Command
|
|
4821
|
-
```typescript
|
|
4822
|
-
interface Command { execute(): void; undo(): void; }
|
|
4823
|
-
|
|
4824
|
-
class AppendCommand implements Command {
|
|
4825
|
-
constructor(private editor: Editor, private text: string) {}
|
|
4826
|
-
execute(): void { this.editor.append(this.text); }
|
|
4827
|
-
undo(): void { this.editor.delete(this.text.length); }
|
|
4828
|
-
}
|
|
4829
|
-
```
|
|
4830
|
-
|
|
4831
|
-
## Best Practices
|
|
4832
|
-
|
|
4833
|
-
- Use patterns to solve specific problems, not everywhere
|
|
4834
|
-
- Combine patterns when appropriate
|
|
4835
|
-
- Favor composition over inheritance
|
|
4836
|
-
- Keep implementations simple
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
---
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
---
|
|
4843
|
-
*Generated by aicgen*
|