@bradtaylorsf/alpha-loop 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/package.json +1 -1
- package/templates/agents/implementer.md +1 -1
- package/templates/skills/skill-creator/SKILL.md +485 -0
- package/templates/skills/api-contracts/SKILL.md +0 -676
- package/templates/skills/api-patterns/SKILL.md +0 -346
- package/templates/skills/api-patterns/examples/complete-rest-api.ts +0 -293
- package/templates/skills/api-patterns/templates/express-router-template.ts +0 -294
- package/templates/skills/jest-mock-patterns/SKILL.md +0 -397
- package/templates/skills/playwright-testing/SKILL.md +0 -124
- package/templates/skills/sqlite-patterns/SKILL.md +0 -229
- package/templates/skills/test-caching/SKILL.md +0 -99
|
@@ -1,676 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: api-contracts
|
|
3
|
-
description: Shared type contracts between backend and frontend to prevent API response mismatches. Essential for all full-stack features.
|
|
4
|
-
auto_load: backend-developer, frontend-developer, integration-specialist
|
|
5
|
-
priority: critical
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# API Contracts Skill
|
|
9
|
-
|
|
10
|
-
## Quick Reference
|
|
11
|
-
|
|
12
|
-
**Use when**: Implementing ANY feature that spans backend and frontend
|
|
13
|
-
|
|
14
|
-
**Purpose**: Prevent API response mismatches by defining shared TypeScript interfaces
|
|
15
|
-
|
|
16
|
-
**Key Pattern**: Single source of truth for API response types used by both backend and frontend
|
|
17
|
-
|
|
18
|
-
**Critical**: This skill prevents the "Test-Reality Gap" where unit tests pass but production fails due to API contract mismatches.
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## The Problem This Solves
|
|
23
|
-
|
|
24
|
-
### Before Shared Contracts (Causes Production Failures)
|
|
25
|
-
|
|
26
|
-
**Backend** (`src/server/routes/dependencies.ts`):
|
|
27
|
-
```typescript
|
|
28
|
-
// Backend developer's assumption
|
|
29
|
-
router.get('/dependency-graph', (req, res) => {
|
|
30
|
-
const nodes = {}; // Returns Object
|
|
31
|
-
features.forEach(f => {
|
|
32
|
-
nodes[f.id] = { id: f.id, dependsOn: f.depends_on_features };
|
|
33
|
-
});
|
|
34
|
-
res.json({ nodes });
|
|
35
|
-
});
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
**Frontend** (`src/client/components/DependencyGraph.tsx`):
|
|
39
|
-
```typescript
|
|
40
|
-
// Frontend developer's assumption
|
|
41
|
-
const graph = await response.json();
|
|
42
|
-
graph.nodes.forEach(node => { // ❌ CRASH: nodes is Object, not Array
|
|
43
|
-
renderNode(node.name); // ❌ CRASH: name field doesn't exist
|
|
44
|
-
});
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
**Result**: All unit tests pass (mocked data is "perfect"), but production fails with runtime errors.
|
|
48
|
-
|
|
49
|
-
### After Shared Contracts (Prevents Failures)
|
|
50
|
-
|
|
51
|
-
**Shared Contract** (`src/shared/types/api-contracts.ts`):
|
|
52
|
-
```typescript
|
|
53
|
-
export interface DependencyGraphResponse {
|
|
54
|
-
nodes: DependencyNode[]; // ✅ Both backend and frontend agree: Array
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface DependencyNode {
|
|
58
|
-
id: number;
|
|
59
|
-
name: string; // ✅ Frontend knows this field exists
|
|
60
|
-
dependsOn: number[];
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**Backend** (TypeScript enforces compliance):
|
|
65
|
-
```typescript
|
|
66
|
-
import { DependencyGraphResponse } from '../shared/types/api-contracts';
|
|
67
|
-
|
|
68
|
-
router.get('/dependency-graph', (req, res) => {
|
|
69
|
-
const response: DependencyGraphResponse = {
|
|
70
|
-
nodes: features.map(f => ({
|
|
71
|
-
id: f.id,
|
|
72
|
-
name: f.description, // ✅ Must include name field
|
|
73
|
-
dependsOn: f.depends_on_features || []
|
|
74
|
-
})) // ✅ Must be Array
|
|
75
|
-
};
|
|
76
|
-
res.json(response); // ✅ TypeScript validates structure
|
|
77
|
-
});
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
**Frontend** (TypeScript knows exact structure):
|
|
81
|
-
```typescript
|
|
82
|
-
import { DependencyGraphResponse } from '../../shared/types/api-contracts';
|
|
83
|
-
|
|
84
|
-
const graph: DependencyGraphResponse = await response.json();
|
|
85
|
-
graph.nodes.forEach(node => { // ✅ TypeScript knows nodes is Array
|
|
86
|
-
renderNode(node.name); // ✅ TypeScript knows name exists
|
|
87
|
-
});
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## Pattern: Creating Shared Contracts
|
|
93
|
-
|
|
94
|
-
### Step 1: Create Shared Types Directory
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
# Create directory structure
|
|
98
|
-
mkdir -p src/shared/types
|
|
99
|
-
|
|
100
|
-
# Create api-contracts.ts
|
|
101
|
-
touch src/shared/types/api-contracts.ts
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Step 2: Define Complete API Response Interface
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
// src/shared/types/api-contracts.ts
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* GET /api/projects/:id/features/dependency-graph
|
|
111
|
-
*
|
|
112
|
-
* Returns dependency graph data for visualization.
|
|
113
|
-
*
|
|
114
|
-
* Used by:
|
|
115
|
-
* - Backend: src/server/routes/dependencies.ts
|
|
116
|
-
* - Frontend: src/client/components/DependencyGraph.tsx
|
|
117
|
-
*
|
|
118
|
-
* @example
|
|
119
|
-
* {
|
|
120
|
-
* nodes: [
|
|
121
|
-
* { id: 1, name: "Authentication", status: "completed", dependsOn: [], blocks: [2] },
|
|
122
|
-
* { id: 2, name: "User Profile", status: "pending", dependsOn: [1], blocks: [] }
|
|
123
|
-
* ],
|
|
124
|
-
* edges: [
|
|
125
|
-
* { from: 2, to: 1, type: "dependency" }
|
|
126
|
-
* ],
|
|
127
|
-
* hasCircularDependencies: false,
|
|
128
|
-
* circularDependencies: []
|
|
129
|
-
* }
|
|
130
|
-
*/
|
|
131
|
-
export interface DependencyGraphResponse {
|
|
132
|
-
/** List of all features as graph nodes */
|
|
133
|
-
nodes: DependencyNode[];
|
|
134
|
-
|
|
135
|
-
/** List of edges connecting nodes */
|
|
136
|
-
edges: DependencyEdge[];
|
|
137
|
-
|
|
138
|
-
/** Map of feature ID to count of unmet dependencies */
|
|
139
|
-
unmetDependencies: Record<number, number>;
|
|
140
|
-
|
|
141
|
-
/** Whether circular dependencies exist */
|
|
142
|
-
hasCircularDependencies: boolean;
|
|
143
|
-
|
|
144
|
-
/** List of detected circular dependency cycles */
|
|
145
|
-
circularDependencies: CircularDependency[];
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Single feature node in dependency graph
|
|
150
|
-
*/
|
|
151
|
-
export interface DependencyNode {
|
|
152
|
-
/** Feature ID */
|
|
153
|
-
id: number;
|
|
154
|
-
|
|
155
|
-
/** Feature name (displayed in graph node) */
|
|
156
|
-
name: string;
|
|
157
|
-
|
|
158
|
-
/** Full feature description (shown in tooltip) */
|
|
159
|
-
description: string;
|
|
160
|
-
|
|
161
|
-
/** Current implementation status */
|
|
162
|
-
status: 'pending' | 'in_progress' | 'completed' | 'blocked';
|
|
163
|
-
|
|
164
|
-
/** Category for color coding */
|
|
165
|
-
category: string;
|
|
166
|
-
|
|
167
|
-
/** List of feature IDs this feature depends on */
|
|
168
|
-
dependsOn: number[];
|
|
169
|
-
|
|
170
|
-
/** List of feature IDs this feature blocks */
|
|
171
|
-
blocks: number[];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Edge connecting two nodes in the graph
|
|
176
|
-
*/
|
|
177
|
-
export interface DependencyEdge {
|
|
178
|
-
/** Source node ID */
|
|
179
|
-
from: number;
|
|
180
|
-
|
|
181
|
-
/** Target node ID */
|
|
182
|
-
to: number;
|
|
183
|
-
|
|
184
|
-
/** Edge type for styling */
|
|
185
|
-
type: 'dependency' | 'blocks';
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Circular dependency detection result
|
|
190
|
-
*/
|
|
191
|
-
export interface CircularDependency {
|
|
192
|
-
/** List of feature IDs forming the cycle */
|
|
193
|
-
cycle: number[];
|
|
194
|
-
|
|
195
|
-
/** Human-readable description */
|
|
196
|
-
description: string;
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Step 3: Document Example Responses
|
|
201
|
-
|
|
202
|
-
Always include `@example` JSDoc tag with realistic response data:
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
/**
|
|
206
|
-
* User authentication response
|
|
207
|
-
*
|
|
208
|
-
* @example
|
|
209
|
-
* {
|
|
210
|
-
* user: {
|
|
211
|
-
* id: 123,
|
|
212
|
-
* email: "user@example.com",
|
|
213
|
-
* name: "John Doe",
|
|
214
|
-
* role: "admin"
|
|
215
|
-
* },
|
|
216
|
-
* token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
217
|
-
* expiresAt: 1640995200000
|
|
218
|
-
* }
|
|
219
|
-
*/
|
|
220
|
-
export interface AuthResponse {
|
|
221
|
-
user: User;
|
|
222
|
-
token: string;
|
|
223
|
-
expiresAt: number;
|
|
224
|
-
}
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
### Step 4: Mark Required vs Optional Fields
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
export interface UpdateUserRequest {
|
|
231
|
-
/** User's email (REQUIRED) */
|
|
232
|
-
email: string;
|
|
233
|
-
|
|
234
|
-
/** User's display name (REQUIRED) */
|
|
235
|
-
name: string;
|
|
236
|
-
|
|
237
|
-
/** User's age (OPTIONAL) */
|
|
238
|
-
age?: number;
|
|
239
|
-
|
|
240
|
-
/** User's bio (OPTIONAL - can be empty string) */
|
|
241
|
-
bio?: string;
|
|
242
|
-
}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
---
|
|
246
|
-
|
|
247
|
-
## Pattern: Backend Usage
|
|
248
|
-
|
|
249
|
-
### Import and Use Shared Contract
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
// src/server/routes/dependencies.ts
|
|
253
|
-
|
|
254
|
-
import { Router } from 'express';
|
|
255
|
-
import {
|
|
256
|
-
DependencyGraphResponse,
|
|
257
|
-
DependencyNode,
|
|
258
|
-
DependencyEdge
|
|
259
|
-
} from '../shared/types/api-contracts';
|
|
260
|
-
import { getDependencyGraph } from '../dal/dependency-resolver';
|
|
261
|
-
|
|
262
|
-
const router = Router();
|
|
263
|
-
|
|
264
|
-
router.get('/api/projects/:id/features/dependency-graph', (req, res) => {
|
|
265
|
-
const projectId = parseInt(req.params.id, 10);
|
|
266
|
-
|
|
267
|
-
// Get data from DAL
|
|
268
|
-
const { nodes, edges, cycles } = getDependencyGraph(projectId);
|
|
269
|
-
|
|
270
|
-
// Build response matching shared contract
|
|
271
|
-
const response: DependencyGraphResponse = {
|
|
272
|
-
nodes: nodes.map((node): DependencyNode => ({
|
|
273
|
-
id: node.id,
|
|
274
|
-
name: node.name || `Feature ${node.id}`,
|
|
275
|
-
description: node.description || '',
|
|
276
|
-
status: node.status,
|
|
277
|
-
category: node.category || 'general',
|
|
278
|
-
dependsOn: node.dependsOn || [],
|
|
279
|
-
blocks: node.blocks || []
|
|
280
|
-
})),
|
|
281
|
-
edges: edges.map((edge): DependencyEdge => ({
|
|
282
|
-
from: edge.from,
|
|
283
|
-
to: edge.to,
|
|
284
|
-
type: edge.type
|
|
285
|
-
})),
|
|
286
|
-
unmetDependencies: {},
|
|
287
|
-
hasCircularDependencies: cycles.length > 0,
|
|
288
|
-
circularDependencies: cycles
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
// TypeScript validates response matches contract
|
|
292
|
-
res.json(response);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
export default router;
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### Ensure ALL Required Fields Present
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
// ❌ WRONG - Missing required fields
|
|
302
|
-
const response: DependencyGraphResponse = {
|
|
303
|
-
nodes: nodes.map(n => ({ id: n.id })) // TypeScript error: missing name, description, status, etc.
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
// ✅ CORRECT - All required fields provided
|
|
307
|
-
const response: DependencyGraphResponse = {
|
|
308
|
-
nodes: nodes.map(n => ({
|
|
309
|
-
id: n.id,
|
|
310
|
-
name: n.description, // Map from database field
|
|
311
|
-
description: n.full_description || '', // Provide default if missing
|
|
312
|
-
status: n.passes ? 'completed' : 'pending',
|
|
313
|
-
category: n.category || 'general',
|
|
314
|
-
dependsOn: n.depends_on_features || [], // Provide empty array if null
|
|
315
|
-
blocks: n.blocks_features || []
|
|
316
|
-
}))
|
|
317
|
-
};
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
---
|
|
321
|
-
|
|
322
|
-
## Pattern: Frontend Usage
|
|
323
|
-
|
|
324
|
-
### Import and Use Shared Contract
|
|
325
|
-
|
|
326
|
-
```typescript
|
|
327
|
-
// src/client/components/DependencyGraph.tsx
|
|
328
|
-
|
|
329
|
-
import React, { useState, useEffect } from 'react';
|
|
330
|
-
import {
|
|
331
|
-
DependencyGraphResponse,
|
|
332
|
-
DependencyNode
|
|
333
|
-
} from '../../shared/types/api-contracts';
|
|
334
|
-
|
|
335
|
-
export function DependencyGraph({ projectId }: { projectId: number }) {
|
|
336
|
-
const [graph, setGraph] = useState<DependencyGraphResponse | null>(null);
|
|
337
|
-
const [error, setError] = useState<string | null>(null);
|
|
338
|
-
|
|
339
|
-
useEffect(() => {
|
|
340
|
-
async function fetchGraph() {
|
|
341
|
-
try {
|
|
342
|
-
const response = await fetch(`/api/projects/${projectId}/features/dependency-graph`);
|
|
343
|
-
|
|
344
|
-
if (!response.ok) {
|
|
345
|
-
throw new Error(`HTTP ${response.status}`);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// TypeScript knows exact shape of response
|
|
349
|
-
const data: DependencyGraphResponse = await response.json();
|
|
350
|
-
setGraph(data);
|
|
351
|
-
} catch (err) {
|
|
352
|
-
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
fetchGraph();
|
|
357
|
-
}, [projectId]);
|
|
358
|
-
|
|
359
|
-
if (error) {
|
|
360
|
-
return <div className="error">Error loading graph: {error}</div>;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (!graph) {
|
|
364
|
-
return <div>Loading...</div>;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return (
|
|
368
|
-
<div className="dependency-graph">
|
|
369
|
-
{/* TypeScript knows nodes is Array with specific fields */}
|
|
370
|
-
{graph.nodes.map((node: DependencyNode) => (
|
|
371
|
-
<div key={node.id} className="graph-node">
|
|
372
|
-
<h3>{node.name}</h3>
|
|
373
|
-
<p>{node.description}</p>
|
|
374
|
-
<span className={`status-${node.status}`}>{node.status}</span>
|
|
375
|
-
</div>
|
|
376
|
-
))}
|
|
377
|
-
</div>
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
### TypeScript Catches Errors at Compile Time
|
|
383
|
-
|
|
384
|
-
```typescript
|
|
385
|
-
// ❌ TypeScript error: nodes might be undefined
|
|
386
|
-
graph.nodes.map(node => ...);
|
|
387
|
-
|
|
388
|
-
// ✅ CORRECT: Check for null
|
|
389
|
-
{graph?.nodes.map(node => ...)}
|
|
390
|
-
|
|
391
|
-
// ❌ TypeScript error: field doesn't exist on DependencyNode
|
|
392
|
-
node.title
|
|
393
|
-
|
|
394
|
-
// ✅ CORRECT: Use correct field from contract
|
|
395
|
-
node.name
|
|
396
|
-
|
|
397
|
-
// ❌ TypeScript error: wrong status value
|
|
398
|
-
node.status === 'done'
|
|
399
|
-
|
|
400
|
-
// ✅ CORRECT: Use value from contract's union type
|
|
401
|
-
node.status === 'completed'
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
---
|
|
405
|
-
|
|
406
|
-
## Pattern: Updating Contracts
|
|
407
|
-
|
|
408
|
-
When API changes, update contract FIRST, then let TypeScript guide you:
|
|
409
|
-
|
|
410
|
-
### 1. Update Shared Contract
|
|
411
|
-
|
|
412
|
-
```typescript
|
|
413
|
-
// src/shared/types/api-contracts.ts
|
|
414
|
-
|
|
415
|
-
export interface DependencyNode {
|
|
416
|
-
id: number;
|
|
417
|
-
name: string;
|
|
418
|
-
description: string;
|
|
419
|
-
status: 'pending' | 'in_progress' | 'completed' | 'blocked';
|
|
420
|
-
category: string;
|
|
421
|
-
dependsOn: number[];
|
|
422
|
-
blocks: number[];
|
|
423
|
-
priority: 'low' | 'medium' | 'high'; // ✅ NEW FIELD ADDED
|
|
424
|
-
}
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
### 2. TypeScript Shows Compilation Errors
|
|
428
|
-
|
|
429
|
-
```bash
|
|
430
|
-
$ pnpm build
|
|
431
|
-
|
|
432
|
-
src/server/routes/dependencies.ts:45:7 - error TS2322:
|
|
433
|
-
Type '{ id: number; name: string; ... }' is not assignable to type 'DependencyNode'.
|
|
434
|
-
Property 'priority' is missing in type '...' but required in type 'DependencyNode'.
|
|
435
|
-
|
|
436
|
-
src/client/components/DependencyGraph.tsx:67:23 - error TS2339:
|
|
437
|
-
Property 'priority' does not exist on type '{ id: number; name: string; ... }'.
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
### 3. Fix Backend First
|
|
441
|
-
|
|
442
|
-
```typescript
|
|
443
|
-
// src/server/routes/dependencies.ts
|
|
444
|
-
|
|
445
|
-
nodes: nodes.map(n => ({
|
|
446
|
-
// ... existing fields
|
|
447
|
-
priority: n.priority || 'medium' // ✅ Add new field
|
|
448
|
-
}))
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
### 4. Fix Frontend Second
|
|
452
|
-
|
|
453
|
-
```typescript
|
|
454
|
-
// src/client/components/DependencyGraph.tsx
|
|
455
|
-
|
|
456
|
-
<div className="graph-node">
|
|
457
|
-
<h3>{node.name}</h3>
|
|
458
|
-
<span className={`priority-${node.priority}`}>{node.priority}</span> {/* ✅ Use new field */}
|
|
459
|
-
</div>
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
### 5. Compilation Succeeds
|
|
463
|
-
|
|
464
|
-
All TypeScript errors resolved. Contract updated everywhere.
|
|
465
|
-
|
|
466
|
-
---
|
|
467
|
-
|
|
468
|
-
## Common Contracts to Create
|
|
469
|
-
|
|
470
|
-
### 1. List/Collection Responses
|
|
471
|
-
|
|
472
|
-
```typescript
|
|
473
|
-
/**
|
|
474
|
-
* GET /api/projects/:id/features
|
|
475
|
-
*/
|
|
476
|
-
export interface FeaturesListResponse {
|
|
477
|
-
features: Feature[];
|
|
478
|
-
total: number;
|
|
479
|
-
page: number;
|
|
480
|
-
pageSize: number;
|
|
481
|
-
}
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
### 2. Single Resource Responses
|
|
485
|
-
|
|
486
|
-
```typescript
|
|
487
|
-
/**
|
|
488
|
-
* GET /api/projects/:id
|
|
489
|
-
*/
|
|
490
|
-
export interface ProjectResponse {
|
|
491
|
-
project: Project;
|
|
492
|
-
}
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
### 3. Create/Update Requests
|
|
496
|
-
|
|
497
|
-
```typescript
|
|
498
|
-
/**
|
|
499
|
-
* POST /api/projects
|
|
500
|
-
*/
|
|
501
|
-
export interface CreateProjectRequest {
|
|
502
|
-
name: string;
|
|
503
|
-
description?: string;
|
|
504
|
-
techStack: {
|
|
505
|
-
frontend: string;
|
|
506
|
-
backend: string;
|
|
507
|
-
database: string;
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
export interface CreateProjectResponse {
|
|
512
|
-
project: Project;
|
|
513
|
-
message: string;
|
|
514
|
-
}
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
### 4. Error Responses
|
|
518
|
-
|
|
519
|
-
```typescript
|
|
520
|
-
/**
|
|
521
|
-
* Standard error response (4xx, 5xx)
|
|
522
|
-
*/
|
|
523
|
-
export interface ErrorResponse {
|
|
524
|
-
error: string;
|
|
525
|
-
message: string;
|
|
526
|
-
statusCode: number;
|
|
527
|
-
details?: Record<string, string[]>; // Validation errors
|
|
528
|
-
}
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
---
|
|
532
|
-
|
|
533
|
-
## Validation Pattern
|
|
534
|
-
|
|
535
|
-
Always validate API responses match contracts:
|
|
536
|
-
|
|
537
|
-
```typescript
|
|
538
|
-
// tests/api/contracts/dependency-graph.contract.test.ts
|
|
539
|
-
|
|
540
|
-
import { DependencyGraphResponse, DependencyNode } from '../../../src/shared/types/api-contracts';
|
|
541
|
-
|
|
542
|
-
describe('Dependency Graph API Contract', () => {
|
|
543
|
-
it('returns response matching DependencyGraphResponse interface', async () => {
|
|
544
|
-
const response = await fetch('http://localhost:4243/api/projects/1/features/dependency-graph');
|
|
545
|
-
expect(response.ok).toBe(true);
|
|
546
|
-
|
|
547
|
-
const data = await response.json();
|
|
548
|
-
|
|
549
|
-
// Validate top-level structure
|
|
550
|
-
expect(data).toHaveProperty('nodes');
|
|
551
|
-
expect(data).toHaveProperty('edges');
|
|
552
|
-
expect(data).toHaveProperty('hasCircularDependencies');
|
|
553
|
-
|
|
554
|
-
// Validate types
|
|
555
|
-
expect(Array.isArray(data.nodes)).toBe(true);
|
|
556
|
-
expect(Array.isArray(data.edges)).toBe(true);
|
|
557
|
-
expect(typeof data.hasCircularDependencies).toBe('boolean');
|
|
558
|
-
|
|
559
|
-
// Validate node structure (if any nodes exist)
|
|
560
|
-
if (data.nodes.length > 0) {
|
|
561
|
-
const node = data.nodes[0];
|
|
562
|
-
expect(typeof node.id).toBe('number');
|
|
563
|
-
expect(typeof node.name).toBe('string');
|
|
564
|
-
expect(typeof node.description).toBe('string');
|
|
565
|
-
expect(['pending', 'in_progress', 'completed', 'blocked']).toContain(node.status);
|
|
566
|
-
expect(Array.isArray(node.dependsOn)).toBe(true);
|
|
567
|
-
expect(Array.isArray(node.blocks)).toBe(true);
|
|
568
|
-
}
|
|
569
|
-
});
|
|
570
|
-
});
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
---
|
|
574
|
-
|
|
575
|
-
## Checklist for Every Full-Stack Feature
|
|
576
|
-
|
|
577
|
-
Before implementation starts:
|
|
578
|
-
- [ ] Shared contract created in `src/shared/types/api-contracts.ts`
|
|
579
|
-
- [ ] All request/response interfaces defined
|
|
580
|
-
- [ ] Example responses documented with `@example` JSDoc
|
|
581
|
-
- [ ] Required vs optional fields clearly marked
|
|
582
|
-
|
|
583
|
-
During backend implementation:
|
|
584
|
-
- [ ] Backend imports shared contract types
|
|
585
|
-
- [ ] Backend response typed as contract interface
|
|
586
|
-
- [ ] All required fields provided (no TypeScript errors)
|
|
587
|
-
- [ ] Contract validation test written
|
|
588
|
-
|
|
589
|
-
During frontend implementation:
|
|
590
|
-
- [ ] Frontend imports shared contract types
|
|
591
|
-
- [ ] API response typed as contract interface
|
|
592
|
-
- [ ] TypeScript shows autocomplete for all fields
|
|
593
|
-
- [ ] No `any` types used
|
|
594
|
-
|
|
595
|
-
Before marking complete:
|
|
596
|
-
- [ ] TypeScript compiles without errors
|
|
597
|
-
- [ ] Contract tests pass
|
|
598
|
-
- [ ] Integration tests pass
|
|
599
|
-
- [ ] Manual browser verification shows no errors
|
|
600
|
-
|
|
601
|
-
---
|
|
602
|
-
|
|
603
|
-
## Common Mistakes to Avoid
|
|
604
|
-
|
|
605
|
-
### Mistake 1: Forgetting to Create Contract
|
|
606
|
-
|
|
607
|
-
```typescript
|
|
608
|
-
// ❌ WRONG - No shared contract
|
|
609
|
-
// backend.ts
|
|
610
|
-
res.json({ nodes: Object.values(graph) });
|
|
611
|
-
|
|
612
|
-
// frontend.ts
|
|
613
|
-
const data: any = await response.json(); // No type safety
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
**Fix**: Create shared contract first, before any implementation.
|
|
617
|
-
|
|
618
|
-
### Mistake 2: Backend and Frontend Use Different Interfaces
|
|
619
|
-
|
|
620
|
-
```typescript
|
|
621
|
-
// ❌ WRONG - Duplicated interfaces
|
|
622
|
-
// backend/types.ts
|
|
623
|
-
interface GraphResponse { nodes: Node[] }
|
|
624
|
-
|
|
625
|
-
// frontend/types.ts
|
|
626
|
-
interface GraphData { items: Node[] } // Different name!
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
**Fix**: Use SINGLE shared interface imported by both.
|
|
630
|
-
|
|
631
|
-
### Mistake 3: Using `any` Instead of Contract
|
|
632
|
-
|
|
633
|
-
```typescript
|
|
634
|
-
// ❌ WRONG - Loses type safety
|
|
635
|
-
const data: any = await response.json();
|
|
636
|
-
|
|
637
|
-
// ✅ CORRECT - Use contract type
|
|
638
|
-
const data: DependencyGraphResponse = await response.json();
|
|
639
|
-
```
|
|
640
|
-
|
|
641
|
-
### Mistake 4: Contract Doesn't Match Reality
|
|
642
|
-
|
|
643
|
-
```typescript
|
|
644
|
-
// ❌ WRONG - Contract says Array, backend returns Object
|
|
645
|
-
export interface Response {
|
|
646
|
-
nodes: Node[]; // Contract says Array
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// backend.ts
|
|
650
|
-
res.json({ nodes: { "1": node1, "2": node2 } }); // Returns Object!
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
**Fix**: Contract tests will catch this mismatch.
|
|
654
|
-
|
|
655
|
-
---
|
|
656
|
-
|
|
657
|
-
## Summary
|
|
658
|
-
|
|
659
|
-
**Core Principle**: API contracts are the single source of truth for communication between backend and frontend.
|
|
660
|
-
|
|
661
|
-
**When to Create**: Before starting ANY full-stack feature implementation.
|
|
662
|
-
|
|
663
|
-
**Who Uses**:
|
|
664
|
-
- Backend developers: Type their responses
|
|
665
|
-
- Frontend developers: Type their API data
|
|
666
|
-
- Integration specialists: Validate responses match
|
|
667
|
-
- QA specialists: Write contract tests
|
|
668
|
-
|
|
669
|
-
**Benefits**:
|
|
670
|
-
- TypeScript catches mismatches at compile time
|
|
671
|
-
- Eliminates runtime errors from unexpected API responses
|
|
672
|
-
- Provides autocomplete in both backend and frontend
|
|
673
|
-
- Self-documenting with JSDoc examples
|
|
674
|
-
- Prevents the Test-Reality Gap
|
|
675
|
-
|
|
676
|
-
**Remember**: Unit tests passing is NOT enough. Shared contracts + contract tests + integration tests = Production confidence.
|