DynoAI API Contract Sync
Problem
Python Flask routes return dicts. TypeScript API clients expect typed interfaces. These diverge silently when:
- •A field is added to a Python response but not the TS interface
- •A field is renamed on one side but not the other
- •A field type changes (e.g.,
inttostr) - •Optional fields become required or vice versa
Where Types Live
| Side | Location | Pattern |
|---|---|---|
| Python routes | api/routes/*.py | return jsonify({...}) dicts |
| Python services | api/services/*.py | Return dicts or dataclasses |
| Python models | api/models/*.py | SQLAlchemy models |
| TS API clients | frontend/src/api/*.ts | Exported interfaces |
| TS shared types | frontend/src/lib/api.ts | Inline interfaces |
| TS domain types | frontend/src/types/*.ts | Exported interfaces |
| TS lib types | frontend/src/lib/types.ts | ManifestData, AnalysisStatus, VEData |
Sync Workflow
Step 1: Identify the Endpoint
Determine which endpoint has drifted. Common triggers:
- •Runtime error: "property X does not exist on type Y"
- •New backend field not showing in frontend
- •User reports missing data in UI
Step 2: Read the Python Source of Truth
Read the Flask route to extract the response shape:
1. Open api/routes/<feature>.py
2. Find the endpoint function
3. Trace the return jsonify({...}) call
4. If it calls a service, trace into api/services/<feature>.py
5. Document every field, its Python type, and whether it's optional
Python-to-TypeScript type mapping:
| Python | TypeScript | Notes |
|---|---|---|
str | string | |
int | number | |
float | number | |
bool | boolean | |
None | null | |
dict | Record<string, unknown> | Or a typed interface |
list[str] | string[] | |
Optional[str] | string | null | Or string? if absent vs null |
datetime (serialized) | string | ISO 8601 format |
Enum.value | String literal union | "pending" | "complete" | "error" |
Step 3: Compare with TypeScript Interface
Read the corresponding TS interface and diff:
1. Open frontend/src/api/<feature>.ts (or frontend/src/lib/api.ts) 2. Find the response interface 3. Compare field-by-field with Python response 4. Mark: MATCH, MISSING_IN_TS, MISSING_IN_PY, TYPE_MISMATCH
Step 4: Generate Updated TypeScript
Update the TS interface to match the Python source of truth. Rules:
- •Python is source of truth for response shapes (backend owns the contract)
- •TypeScript is source of truth for request shapes (frontend owns what it sends)
- •Add fields as optional (
field?: type) if they were recently added to Python and might not be present in older API responses - •Preserve JSDoc comments on existing fields
- •Keep the interface in its current file -- don't move types between files during sync
Step 5: Verify Consumers
After updating the interface, check all consumers:
Search for: the interface name and the API function name Files to check: frontend/src/hooks/, frontend/src/components/, frontend/src/pages/ Fix: any property access that doesn't match the updated interface
Common Sync Scenarios
Scenario A: New field added to Python response
# Python added "convergence_rate" to response
return jsonify({
"status": "complete",
"results": {...},
"convergence_rate": 0.85, # NEW
})
Fix: Add to TS interface as optional (backward compat):
export interface AnalysisResult {
status: string;
results: Record<string, unknown>;
convergence_rate?: number; // Added in v1.3
}
Scenario B: Python field renamed
# "error_msg" renamed to "error_message"
return jsonify({"error_message": str(e)}) # was "error_msg"
Fix: Update TS interface, then search-and-replace all usages of the old field name in frontend code.
Scenario C: Nested object shape changed
# "results" changed from flat dict to nested structure
return jsonify({
"results": {
"summary": {...}, # was flat fields
"details": {...}, # was flat fields
}
})
Fix: Create nested TS interfaces:
export interface ResultsSummary {
// summary fields
}
export interface ResultsDetails {
// detail fields
}
export interface AnalysisResult {
results: {
summary: ResultsSummary;
details: ResultsDetails;
};
}
Bulk Audit Checklist
When auditing all API contracts at once:
Audit Progress: - [ ] /api/analyze (app.py -> lib/api.ts) - [ ] /api/status/<run_id> (app.py -> lib/api.ts) - [ ] /api/ve-data/<run_id> (app.py -> lib/api.ts) - [ ] /api/apply (app.py -> lib/api.ts) - [ ] /api/rollback (app.py -> lib/api.ts) - [ ] /api/jetstream/* (routes/jetstream.py -> api/jetstream.ts) - [ ] /api/timeline/* (routes/timeline.py -> api/timeline.ts) - [ ] /api/wizards/* (routes/wizards.py -> api/wizards.ts) - [ ] /api/jetdrive/* (routes/jetdrive.py -> hooks/useJetDriveLive.ts) - [ ] /api/virtual-tune/* (routes/virtual_tune.py -> api or hooks) - [ ] /api/transient/* (routes/transient.py -> api/transient.ts) - [ ] /api/ea/* (routes/engine_analyzer.py -> api or hooks) - [ ] /api/nextgen/* (routes/nextgen.py -> lib/api.ts) - [ ] /api/reports/* (routes/reports.py -> api/reports.ts) - [ ] /api/powercore/* (routes/powercore.py -> lib/api.ts)
Prevention
To prevent future drift, when adding a new endpoint:
- •Define the response shape in Python first (even as a comment or dataclass)
- •Create the TS interface before writing the API client function
- •Use the
dynoai-fullstack-featureskill which generates both sides together