Skip to content

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

  1. OpenAPI spec lives in backend - iOS just consumes a copy
  2. Generator runs automatically during Swift build
  3. Mappings layer bridges generated DTOs → domain models
  4. 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

bash
# Search for similar endpoint usage
rg "client\.(createTask|updateTask)" Sources/APIClientLive/

Step 2: Check for Mappings extension

bash
ls Sources/APIClientLive/Mappings/Mappings+*.swift

Step 3: Use mapping pattern, NEVER manual construction

swift
// ❌ 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

  1. Get updated openapi.yaml from backend team
  2. Replace Sources/APIClientLive/openapi.yaml
  3. Run swift build --target APIClientLive to trigger generator
  4. Check for compilation errors

Phase 2: Add Protocol Method

  1. Open Sources/APIClient/APIClient.swift
  2. 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

  1. Open Sources/APIClientLive/APIClient+Live.swift
  2. 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

  1. If new domain type, add to Sources/SharedModels/
  2. Create/extend Mappings+Domain.swift:
    swift
    extension DomainModel {
        func toAPI() -> Components.Schemas.DomainModelDTO {
            // Map domain → DTO
        }
    }
    
    extension Components.Schemas.DomainModelDTO {
        func toDomain() -> DomainModel {
            // Map DTO → domain
        }
    }

Phase 5: Verify CI

  1. Push branch
  2. CI runs ci-gate.yml → detects contract changes
  3. Builds APIClientLive target
  4. Fails fast if generator errors

⚠️ Multipart Upload Gotcha

Only 2 endpoints use multipart/form-data:

  • setUserProfilePhoto
  • attachQuestionAnswerPhoto

Schema names are path-derived, not operationId-derived:

swift
// ❌ 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_post

Always verify: Search openapi.yaml for multipart/form-data to confirm schema name.

⚠️ Argument Label Changes

Generator updates can silently change parameter labels:

swift
// Before
.init(id: courseID.rawValue)

// After generator run
.init(course_id: courseID.rawValue)  // Label changed!

Mitigation:

  1. After contract sync, immediately run swift build
  2. Fix all compilation errors before committing
  3. CI will catch if you miss any

⚠️ Operations.* vs Components.Schemas.*

Some endpoints use Operations.operationId.* types directly:

  • listTasks
  • createQuestionAnswerComment
  • listQuestionAnswerComments
  • listQuestionAnswers

Check existing usage before assuming Components.Schemas.*.


Testing Strategy

Local Validation

bash
# 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 build

CI Validation

ci-gate.yml automatically:

  1. Detects changes to Sources/APIClient* or openapi.yaml
  2. Runs targeted build of APIClientLive
  3. 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

ErrorCauseFix
Cannot find 'Components' in scopeGenerator didn't runswift build --target APIClientLive
Argument 'foo' not foundParameter label changedCheck generated types, update call
Cannot convert value of type 'X' to 'Y'Wrong DTO typeUse correct Components.Schemas.* or Operations.*
Body_xyz not foundWrong multipart schema nameVerify path-derived name in openapi.yaml

File Locations Quick Reference

PathPurpose
Sources/APIClientLive/openapi.yamlOpenAPI spec (source of truth for iOS)
Sources/APIClient/APIClient.swiftProtocol interface (~80 methods)
Sources/APIClientLive/APIClient+Live.swiftImplementation using generated client
Sources/APIClientLive/Mappings/*.swiftDTO ↔ Domain conversions (8 files)
Sources/SharedModels/*.swiftDomain models
.github/workflows/ci-gate.ymlContract change validation

Related Documentation

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.

AI-first documentation for Nuance iOS