OpenAPI Contract Flow
For Humans: Visual Overview
┌─────────────────────────────────────────────────────────────┐
│ Backend (nuance-api) │
│ │
│ FastAPI Routes → Auto-generated openapi.yaml (3350 lines) │
└───────────────────────────┬─────────────────────────────────┘
│
│ Manual sync
│ (Copy openapi.yaml)
▼
┌─────────────────────────────────────────────────────────────┐
│ iOS Repo (nuance-ios) │
│ │
│ Sources/APIClientLive/openapi.yaml │
│ │ │
│ │ Swift Package Manager build │
│ ▼ │
│ Swift OpenAPI Generator Plugin │
│ │ │
│ ├─► Generated/Types.swift (Components.Schemas) │
│ └─► Generated/Client.swift (Operations) │
│ │ │
│ ▼ │
│ Sources/APIClientLive/APIClient+Live.swift │
│ Implements APIClient protocol: │
│ - Calls generated operations │
│ - Uses Mappings/*.swift extensions │
│ │ │
│ ▼ │
│ Mappings/*.swift (8 files) │
│ - Mappings+User.swift │
│ - Mappings+Task.swift │
│ - Mappings+Question.swift │
│ - etc. │
│ Extensions with .toAPI() / .toDomain() methods │
│ │ │
│ ▼ │
│ Sources/SharedModels/*.swift │
│ Domain models: User, AppTask, Course, etc. │
└─────────────────────────────────────────────────────────────┘Key Points
- OpenAPI spec lives in backend - iOS just consumes a copy
- Generator runs automatically during Swift build
- Mappings layer bridges generated DTOs → domain models
- CI validates contract changes via
ci-gate.yml
For AI Agents: Critical Checklist
Common Failure Modes
These patterns have caused production issues multiple times. Follow checklist exactly.
✅ BEFORE Adding New API Call
Step 1: Find existing pattern
# Search for similar endpoint usage
rg "client\.(createTask|updateTask)" Sources/APIClientLive/Step 2: Check for Mappings extension
ls Sources/APIClientLive/Mappings/Mappings+*.swiftStep 3: Use mapping pattern, NEVER manual construction
// ❌ WRONG - Manual construction
try await client.createTask(body: .json(.init(
title: task.title,
duration: task.duration, // Parameter name might be wrong!
start_time: task.startTime // Parameter might not exist!
)))
// ✅ CORRECT - Use mapping extension
let request = task.toAPI() // Defined in Mappings+Task.swift
try await client.createTask(body: .json(request))✅ WHEN Adding New Endpoint from Backend
Phase 1: Contract Sync
- Get updated
openapi.yamlfrom backend team - Replace
Sources/APIClientLive/openapi.yaml - Run
swift build --target APIClientLiveto trigger generator - Check for compilation errors
Phase 2: Add Protocol Method
- Open
Sources/APIClient/APIClient.swift - Add new method signature:swift
@DependencyClient public struct APIClient: Sendable { // Existing methods... public var newEndpoint: @Sendable (InputType) async throws -> OutputType }
Phase 3: Implement in Live
- Open
Sources/APIClientLive/APIClient+Live.swift - Implement using generated operation:swift
newEndpoint: { input in let request = input.toAPI() // Create mapping if needed let response = try await client.newOperationId(body: .json(request)) return try response.ok.body.json.toDomain() // Create mapping if needed }
Phase 4: Create Mappings
- If new domain type, add to
Sources/SharedModels/ - Create/extend
Mappings+Domain.swift:swiftextension DomainModel { func toAPI() -> Components.Schemas.DomainModelDTO { // Map domain → DTO } } extension Components.Schemas.DomainModelDTO { func toDomain() -> DomainModel { // Map DTO → domain } }
Phase 5: Verify CI
- Push branch
- CI runs
ci-gate.yml→ detects contract changes - Builds
APIClientLivetarget - Fails fast if generator errors
⚠️ Multipart Upload Gotcha
Only 2 endpoints use multipart/form-data:
setUserProfilePhotoattachQuestionAnswerPhoto
Schema names are path-derived, not operationId-derived:
// ❌ WRONG - Assumed operationId naming
typealias PhotoBody = Components.Schemas.Body_setUserProfilePhoto
// ✅ CORRECT - Path-derived (check openapi.yaml!)
typealias PhotoBody = Components.Schemas.Body_upload_photo_v1_users_upload_photo_postAlways verify: Search openapi.yaml for multipart/form-data to confirm schema name.
⚠️ Argument Label Changes
Generator updates can silently change parameter labels:
// Before
.init(id: courseID.rawValue)
// After generator run
.init(course_id: courseID.rawValue) // Label changed!Mitigation:
- After contract sync, immediately run
swift build - Fix all compilation errors before committing
- CI will catch if you miss any
⚠️ Operations.* vs Components.Schemas.*
Some endpoints use Operations.operationId.* types directly:
listTaskscreateQuestionAnswerCommentlistQuestionAnswerCommentslistQuestionAnswers
Check existing usage before assuming Components.Schemas.*.
Testing Strategy
Local Validation
# 1. Build APIClientLive target only (fast)
swift build --target APIClientLive
# 2. Run contract-specific tests (if they exist)
swift test --filter APIClientLiveTests
# 3. Check for compilation errors in features
swift buildCI Validation
ci-gate.yml automatically:
- Detects changes to
Sources/APIClient*oropenapi.yaml - Runs targeted build of
APIClientLive - Fails fast on generator errors
If CI fails:
- Check generator output in logs
- Verify openapi.yaml syntax
- Ensure all Body_ schema names match spec
Common Error Messages
| Error | Cause | Fix |
|---|---|---|
Cannot find 'Components' in scope | Generator didn't run | swift build --target APIClientLive |
Argument 'foo' not found | Parameter label changed | Check generated types, update call |
Cannot convert value of type 'X' to 'Y' | Wrong DTO type | Use correct Components.Schemas.* or Operations.* |
Body_xyz not found | Wrong multipart schema name | Verify path-derived name in openapi.yaml |
File Locations Quick Reference
| Path | Purpose |
|---|---|
Sources/APIClientLive/openapi.yaml | OpenAPI spec (source of truth for iOS) |
Sources/APIClient/APIClient.swift | Protocol interface (~80 methods) |
Sources/APIClientLive/APIClient+Live.swift | Implementation using generated client |
Sources/APIClientLive/Mappings/*.swift | DTO ↔ Domain conversions (8 files) |
Sources/SharedModels/*.swift | Domain models |
.github/workflows/ci-gate.yml | Contract change validation |
Related Documentation
- API Endpoints Reference - All 52 operationIds
- TCA Backend Contract Analysis skill - Agent workflow
- operationid-inventory.md - Implementation status
Backend Contract Sync
OpenAPI spec is manually synced from backend. If spec is stale (>7 days), generated types may not match backend reality. Always verify with backend team before major feature work.