Rebuilding MistKit with Claude Code - From CloudKit Docs to Type-Safe Swift (Part 1)

In my previous article about Building SyntaxKit with AI, I explored how with the help of Claude Code I could transform SwiftSyntax's 80+ lines of verbose API calls into 10 lines of elegant, declarative Swift.

I saw how Claude Code could easily replace and understand patterns. That's when I decided to explore the idea of updating MistKit, my library for server-side CloudKit application and see how Claude Code can help.


In this series:


πŸ“š View Documentation | πŸ™ GitHub Repository

The Decision to Rebuild

I had a couple of use cases where MistKit running in the cloud would allow me to store data in a public database. However I hadn't touched the library in a while.

By now, Swift had transformed while MistKit stood still:

MistKit, frozen in 2021, couldn't take advantage of any of this.

The Game Changer: swift-openapi-generator

At WWDC 2023, Apple announced swift-openapi-generatorβ€”a tool that reads OpenAPI specifications and automatically generates type-safe Swift client code. This single tool made the MistKit rebuild feasible. What was missing was an OpenAPI spec. If I had that I could easily create a library which made the necessary calls to CloudKit as needed, as well as compatibility with server-side (AsyncHTTPClient) or client-side (URLSession) APIs .

That's where Claude Code came in.

Learning from SyntaxKit's Pattern

With my work on SyntaxKit, I could see that if I fed sufficient documentation on an API to an LLM, it can understand how to develop against it. There may be issues along the way. However, any failures come with the ability to learn and adapt either with internal documentation or writing sufficient tests.

Just as I was able to simplify SwiftSyntax into a simpler API with SyntaxKit, I can have an LLM create an OpenAPI spec for CloudKit.


The pattern was clear: give Claude the right context, and it could translate Apple's documentation into a usable OpenAPI spec. SyntaxKit taught me that code generation works best when you have a clear source of truthβ€”for SyntaxKit it was SwiftSyntax ASTs, for MistKit it would be CloudKit's REST API documentation. The abstraction layer would come later.

The rebuild was ready to begin.

CloudKit Web Services Documentation Site

Building with Claude Code

I needed a way for Claude Code to understand how the CloudKit REST API worked. There was one main document I usedβ€”the CloudKit Web Services Documentation Site. The CloudKit Web Services Documentation Site, which hasn't been updated since June of 2016, contains the most thorough documentation on how the REST API works and hopefully can provide enough for Claude to start crafting the OpenAPI spec.

By running the site (as well as the swift-openapi-generator documentation) through llm.codes, saving the exported markdown documentation in the .claude/docs directory and letting Claude Code know about it (i.e. add a reference to it in Claude.md), I could now start having Claude Code translate the documentation into a usable API.

Setting Up Claude Code for MistKit

Before diving in, here's what you need to understand about working with Claude Code:

Documentation Export with llm.codes I used llm.codes (mentioned in my SyntaxKit article) to convert Apple's web documentation into markdown format that Claude can easily understand. This tool crawls documentation sites and exports them as clean markdown files. It also works with DocC documentation from Swift packages, making it easy to give Claude context about any Swift library's API.

Claude Code's Context System Claude Code uses a simple but powerful context system:

This gives Claude the context it needs to understand CloudKit's API without you having to paste documentation repeatedly in every conversation.

.claude/docs
β”œβ”€β”€ cktool-full.md              # Complete CloudKit CLI tool documentation
β”œβ”€β”€ cktool.md                   # Condensed CloudKit CLI reference
β”œβ”€β”€ cktooljs-full.md            # Full CloudKitJS documentation
β”œβ”€β”€ cktooljs.md                 # CloudKitJS quick reference
β”œβ”€β”€ cloudkit-public-database-architecture.md
β”œβ”€β”€ cloudkit-schema-plan.md
β”œβ”€β”€ cloudkit-schema-reference.md
β”œβ”€β”€ cloudkitjs.md               # JavaScript SDK documentation
β”œβ”€β”€ data-sources-api-research.md
β”œβ”€β”€ firmware-wiki.md
β”œβ”€β”€ https_-swiftpackageindex.com-apple-swift-log-main-documentation-logging.md
β”œβ”€β”€ https_-swiftpackageindex.com-apple-swift-openapi-generator-1.10.3-documentation-swift-openapi-generator.md
β”œβ”€β”€ https_-swiftpackageindex.com-brightdigit-SyndiKit-0.6.1-documentation-syndikit.md
β”œβ”€β”€ mobileasset-wiki.md
β”œβ”€β”€ protocol-extraction-continuation.md
β”œβ”€β”€ QUICK_REFERENCE.md
β”œβ”€β”€ README.md
β”œβ”€β”€ schema-design-workflow.md
β”œβ”€β”€ sosumi-cloudkit-schema-source.md
β”œβ”€β”€ SUMMARY.md
β”œβ”€β”€ testing-enablinganddisabling.md
└── webservices.md              # Primary CloudKit Web Services REST API documentation

Note: Files with "-full" suffix contain complete documentation exported from llm.codes, while shorter versions are condensed for quicker reference. The swift-openapi-generator docs were essential for understanding type overrides and middleware configuration.

Why OpenAPI + swift-openapi-generator?

With swift-openapi-generator available (announced WWDC 2023), the path forward became clear:

  1. Create OpenAPI specification from CloudKit documentation
    • Translate Apple's prose docs β†’ Machine-readable YAML
    • Every endpoint, parameter, response type formally defined
  1. Let swift-openapi-generator generate the client
    • Run swift build β†’ 10,476 lines of type-safe networking code appear
    • Request/response types (Codable structs)
    • API client methods (async/await)
    • Type-safe enums, JSON handling, URL building
    • Configuration: openapi-generator-config.yaml + Swift Package Manager build plugin
  1. Build clean abstraction layer on top
    • Wrap generated code in friendly, idiomatic Swift API
    • Add TokenManager for authentication
    • CustomFieldValue for CloudKit's polymorphic types

By following spec-driven development, we had many benefits:

Challenge #1: Type System Polymorphism

CloudKit fields are dynamically typedβ€”one field can be STRING, INT64, DOUBLE, TIMESTAMP, BYTES, REFERENCE, ASSET, LOCATION, or LIST. But OpenAPI is statically typed. How do we model this polymorphism?

Me: "Here's CloudKit's field value structure from Apple's docs.
     A field can have value of type STRING, INT64, DOUBLE, TIMESTAMP,
     BYTES, REFERENCE, ASSET, LOCATION, LIST..."

Claude: "This is a discriminated union. Try modeling with oneOf in OpenAPI:
         The value property can be oneOf the different types,
         and the type field acts as a discriminator."

Me: "Good start, but there's a CloudKit quirk: ASSETID is different
     from ASSET. ASSET has full metadata, ASSETID is just a reference."

Claude: "Interesting! You'll need a type override in the generator config:
         typeOverrides:
           schemas:
             FieldValue: CustomFieldValue
         Then implement CustomFieldValue to handle ASSETID specially."

Me: "Perfect. Can you generate test cases for all field types?"

Claude: "Here are test cases for STRING, INT64, DOUBLE, TIMESTAMP,
         BYTES, REFERENCE, ASSET, ASSETID, LOCATION, and LIST..."

Having developed MistKit previously, I understood the challenge of various field types and the difficulty in expressing that in Swift. This is a common challenge in Swift with JSON data.

Claude's suggestion of typeOverrides was the breakthroughβ€”instead of fighting OpenAPI's type system, we'd let the generator create basic types, then override with our custom implementation that handles CloudKit's quirks.

Understanding ASSET vs ASSETID

CloudKit uses two different type discriminators for asset fields:

ASSET - Full asset metadata returned by CloudKit

ASSETID - Asset reference placeholder

At the end of the day, both decode to the same AssetValue structure, but CloudKit distinguishes them with different type strings ("ASSET" vs "ASSETID"). Our custom implementation handles this elegantly:

internal struct CustomFieldValue: Codable, Hashable, Sendable {
    internal enum FieldTypePayload: String, Codable, Sendable {
        case asset = "ASSET"
        case assetid = "ASSETID"  // Both decode to AssetValue
        case string = "STRING"
        case int64 = "INT64"
        // ... more types
    }

    internal let value: CustomFieldValuePayload
    internal let type: FieldTypePayload?
}

Using the CustomFieldValue with the power of openapi-generator typeOverides allows us to implement the specific quirks of CloudKit field values.

Challenge #2: Authentication Complexity

The next challenge was dealing with the 3 different methods of authentication:

  1. API Token - Container-level access
    • Query parameter: ckAPIToken
    • Simplest method
    • A starting point for Web Auth Token
  1. Web Auth Token - User-specific access
    • Two query parameters: ckAPIToken + ckWebAuthToken
    • For private database access
  1. Server-to-Server - Public Database Access
    • ECDSA P-256 signature in Authorization header
    • Most complex, most secure

This became a complexity problem when trying to model it in OpenAPI. What Claude suggested was to use the ClientMiddleware API to handle authentication dynamically rather than relying on generator's built-in auth. The meant we used:

  1. OpenAPI: Define all three securitySchemes but make endpoint security optional (security: [])
  2. Middleware: Implement AuthenticationMiddleware that inspects TokenManager at runtime
  3. TokenManager Protocol: Three implementations (API, WebAuth, ServerToServer)
  4. Runtime Selection: Client chooses auth method via TokenManager injection

Here's what it looks like:

internal struct AuthenticationMiddleware: ClientMiddleware {
    internal let tokenManager: any TokenManager

    func intercept(...) async throws -> (HTTPResponse, HTTPBody?) {
        guard let credentials = try await tokenManager.getCurrentCredentials() else {
            throw TokenManagerError.invalidCredentials(.noCredentialsAvailable)
        }

        var modifiedRequest = request

        switch credentials.method {
        case .apiToken(let token):
            // Add ckAPIToken to query parameters
        case .webAuthToken(let apiToken, let webToken):
            // Add both tokens to query parameters
        case .serverToServer(let keyID, let privateKey):
            // Sign request with ECDSA
            // Add Authorization header with signature
        }

        return try await next(modifiedRequest, body, baseURL)
    }
}

This helps because:

While this looks like it will work I wanted to make absolute sure. When using an LLM it's important to have code which actually proves that it works and not just unit test. So I created a simple command line tool which would to both write and read from a public and private database:

Me: "Can you run MistDemo to test the actual functionality?"

Claude: *[Runs demo successfully]*
"Perfect! The MistDemo ran successfully:
 - Web Authentication - Used API token + web auth token
 - CloudKit API Calls - Made multiple successful requests
 - HTTP Integration - AuthenticationMiddleware properly added all required parameters
 - Data Retrieval - Successfully retrieved TodoItem records from CloudKit

 The authentication layer is working as designed. All three authentication methods have a solid foundation!"

We'll touch more on this approach in the next blog post.

Challenge #3: Error Handling

CloudKit returns over 9 different HTTP status codes, each with nested error details including serverErrorCode, reason, uuid, and sometimes redirectURL or retryAfter. What would be nice is if we can parse these in a Swift-y way, taking advantage of Swift 6 features like typed throws for more precise error handling.

According to Apple's Documentation:

Record Fetch Error Dictionary The error dictionary describing a failed operation with the following keys:

Based on this, I had Claude create an openapi entry on this:

components:
  schemas:
    ErrorResponse:
      type: object
      description: Error response object
      properties:
        uuid:
          type: string
          description: Unique error identifier for support
        serverErrorCode:
          type: string
          enum:
            - ACCESS_DENIED
            - ATOMIC_ERROR
            - AUTHENTICATION_FAILED
            - AUTHENTICATION_REQUIRED
            - BAD_REQUEST
            - CONFLICT
            - EXISTS
            - INTERNAL_ERROR
            - NOT_FOUND
            - QUOTA_EXCEEDED
            - THROTTLED
            - TRY_AGAIN_LATER
            - VALIDATING_REFERENCE_ERROR
            - ZONE_NOT_FOUND
        reason:
          type: string
        redirectURL:
          type: string

  responses:
    BadRequest:
      description: Bad request (400) - BAD_REQUEST, ATOMIC_ERROR
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

    Unauthorized:
      description: Unauthorized (401) - AUTHENTICATION_FAILED
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

    # ... additional error responses for 403, 404, 409, 412, 413, 421, 429, 500, 503

Claude was able to translate the documentation into:

  1. Error Code Enum: Converted prose list of error codes to explicit enum
  2. HTTP Status Mapping: Created reusable response components for each HTTP status
  3. Consistent Schema: All errors use same ErrorResponse schema
  4. Status Documentation: Linked HTTP statuses to CloudKit error codes in descriptions

This enables:

Here's how it's mapped:

HTTP StatusCloudKit Error CodesClient Action
400 Bad RequestBAD_REQUEST, ATOMIC_ERRORFix request parameters or retry non-atomically
401 UnauthorizedAUTHENTICATION_FAILEDRe-authenticate or check credentials
403 ForbiddenACCESS_DENIEDUser lacks permissions
404 Not FoundNOT_FOUND, ZONE_NOT_FOUNDVerify resource exists
409 ConflictCONFLICT, EXISTSFetch latest version and retry, or use force operations
412 Precondition FailedVALIDATING_REFERENCE_ERRORReferenced record doesn't exist
413 Request Too LargeQUOTA_EXCEEDEDReduce request size or upgrade quota
429 Too Many RequestsTHROTTLEDImplement exponential backoff
500 Internal ErrorINTERNAL_ERRORRetry with backoff
503 Service UnavailableTRY_AGAIN_LATERTemporary issue, retry later

This structured error handling enables the generated client to provide specific, actionable error messages rather than generic HTTP failures. Developers get type-safe error codes, HTTP status mapping, and clear guidance on how to handle each error condition.

Challenge #4: API Ergonomics

The generated OpenAPI client works, but it's not exactly ergonomic. Here's what a simple query looks like with the raw generated code:

// Verbose generated API
let input = Operations.queryRecords.Input(
    path: .init(
        version: "1",
        container: "iCloud.com.example.MyApp",
        environment: Components.Parameters.environment.production,
        database: Components.Parameters.database._private
    ),
    headers: .init(accept: [.json]),
    body: .json(.init(
        query: .init(recordType: "User")
    ))
)

let response = try await client.queryRecords(input)

switch response {
case .ok(let okResponse):
    let queryResponse = try okResponse.body.json
    // Process records...
default:
    // Handle errors...
}

The problem is there's too much boilerplate for simple operations when we can clean this up with a nicer abstraction. The solution was to build a three-layer architecture that keeps the generated code internal and exposes a clean public API:

Three-layer architecture showing User Code (public API), MistKit Abstraction (internal), and Generated OpenAPI Client (internal)

So now it can look something like this:

// Clean, idiomatic Swift
let service = try CloudKitService(
    container: "iCloud.com.example.MyApp",
    environment: .production,
    database: .private,
    tokenManager: tokenManager
)

let records = try await service.queryRecords(
    recordType: "User",
    filter: .equals("status", .string("active"))
)

// Type-safe field access
for record in records {
    if let name = record.fields["name"]?.stringValue {
        print("User: \(name)")
    }
}

In this case, we create a few abstraction to help:

This means the generated code stays internal while users interact with the more friendly API.

The Iterative Workflow with Claude

This process of building and refining was iterative when working with Claude Code:

  1. I draft the structure
    • Provide CloudKit domain knowledge and desired API
  1. Claude expands
    • Fills in request/response schemas
    • Generates boilerplate for similar endpoints
    • Creates consistent patterns
  1. I review for CloudKit accuracy
    • Check against Apple docs
    • Add edge cases and CloudKit quirks
    • Refine error responses
    • Define integration and unit tests for verification
  1. Claude validates consistency
    • Catches schema mismatches
    • Suggests improvements
  1. Iterate until complete

Let's take for instance, this conversation I had with Claude:

Me: "Here's the query endpoint from Apple's docs"

Claude: *[Creates complete OpenAPI definition]*
"Here's a complete OpenAPI definition with request/response schemas"

Me: "Add `resultsLimit` validation and `continuationMarker` for pagination"

Claude: *[Updates definition with pagination support]*
"Updated, and I noticed the `zoneID` should be optional"

By providing my own experience with great Swift APIs and Claude's ability at applying patterns, I quickly build a library that's friendly to use.

Building MistKit from Scratch with Claude Code

With Claude Code, I could easily create an openapi document based on the Apple's documentation. With my guidance and understanding with the REST API and good Swift design, I could guide Claude through issues like:

This will make it much easier to continue future features with MistKit and enabling me to create some server-side application for my apps.

What's Next

After three months of collaboration with Claude (representing significant acceleration over manual development), I had:

The OpenAPI spec was complete. The generated client compiled. The abstraction layer was elegant. Unit tests passed.

How Claude Code Accelerated Development:

What would have likely taken 6-12 months of solo development was compressed into 3 months of side-project collaboration, with Claude handling repetitive tasks while I focused on architecture, CloudKit-specific quirks, and real-world testing.

However I really needed to put it the test in my actual uses. In the next post, I'll talk about find flaws in MistKit by actually consuming my library with help from Claude Code. I'll be building a couple of command line tools for easily uploading data for Bushel and a future RSS Reader to the public database. By doing this I'll understand Claude's limitation, benefits and how to workaround those.