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:
- Building SyntaxKit with AI
- Rebuilding MistKit with Claude Code (Part 1)
- Coming soon: Rebuilding MistKit with Claude Code (Part 2)
π View Documentation | π GitHub Repository
- The Decision to Rebuild
- The Game Changer: swift-openapi-generator
- Learning from SyntaxKit's Pattern
- Building with Claude Code
- Why OpenAPI + swift-openapi-generator?
- Challenge #1: Type System Polymorphism
- Challenge #2: Authentication Complexity
- Challenge #3: Error Handling
- Challenge #4: API Ergonomics
- The Iterative Workflow with Claude
- What's Next
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:
- Swift 6 with strict concurrency checking
- async/await as standard (not experimental)
- Server-side Swift maturity (Vapor 4, swift-nio, AWS Lambda)
- Modern patterns expected (Result types, AsyncSequence, property wrappers)
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.

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:
.claude/docs/- Store reference documentation (like CloudKit API docs, swift-openapi-generator guides).claude/CLAUDE.mdorCLAUDE.md- Reference these docs so Claude knows to use them as context
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:
- Create OpenAPI specification from CloudKit documentation
- Translate Apple's prose docs β Machine-readable YAML
- Every endpoint, parameter, response type formally defined
- 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
- Run
- 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:
- Type safety (if it compiles, it's valid CloudKit usage)
- Completeness (every endpoint defined)
- Maintainability (spec changes = regenerate code)
- No manual JSON parsing or networking boilerplate
- Cross-platform (macOS, iOS, Linux, server-side Swift)
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
- Appears in: Query responses, lookup responses, modification responses
- Contains:
fileChecksum,size,downloadURL,wrappingKey,receipt - Use case: When you need to download or verify the asset file
ASSETID - Asset reference placeholder
- Appears in: Record creation/update requests
- Contains: Same structure as ASSET, but typically only
downloadURLpopulated - Use case: When you're referencing an already-uploaded asset
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:
- API Token - Container-level access
- Query parameter:
ckAPIToken - Simplest method
- A starting point for Web Auth Token
- Query parameter:
- Web Auth Token - User-specific access
- Two query parameters:
ckAPIToken+ckWebAuthToken - For private database access
- Two query parameters:
- 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:
- OpenAPI: Define all three
securitySchemesbut make endpoint security optional (security: []) - Middleware: Implement
AuthenticationMiddlewarethat inspectsTokenManagerat runtime - TokenManager Protocol: Three implementations (API, WebAuth, ServerToServer)
- 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:
- β Generator doesn't need to handle auth complexity
- β We control authentication at runtime
- β Easy to test (inject mock TokenManager)
- β Supports all three methods seamlessly
- β Can switch auth methods without code changes
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:
recordName: The name of the record that the operation failed on.reason: A string indicating the reason for the error.serverErrorCode: A string containing the code for the error that occurred. For possible values, see Error Codes.retryAfter: The suggested time to wait before trying this operation again.uuid: A unique identifier for this error.redirectURL: A redirect URL for the user to securely sign in.
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:
- Error Code Enum: Converted prose list of error codes to explicit enum
- HTTP Status Mapping: Created reusable response components for each HTTP status
- Consistent Schema: All errors use same
ErrorResponseschema - Status Documentation: Linked HTTP statuses to CloudKit error codes in descriptions
This enables:
- Type-Safe Error Handling: Generated code includes all possible error codes
- Automatic Deserialization: Errors automatically parsed to correct type
- Centralized Definitions: Define once, reference everywhere
Here's how it's mapped:
| HTTP Status | CloudKit Error Codes | Client Action |
|---|---|---|
| 400 Bad Request | BAD_REQUEST, ATOMIC_ERROR | Fix request parameters or retry non-atomically |
| 401 Unauthorized | AUTHENTICATION_FAILED | Re-authenticate or check credentials |
| 403 Forbidden | ACCESS_DENIED | User lacks permissions |
| 404 Not Found | NOT_FOUND, ZONE_NOT_FOUND | Verify resource exists |
| 409 Conflict | CONFLICT, EXISTS | Fetch latest version and retry, or use force operations |
| 412 Precondition Failed | VALIDATING_REFERENCE_ERROR | Referenced record doesn't exist |
| 413 Request Too Large | QUOTA_EXCEEDED | Reduce request size or upgrade quota |
| 429 Too Many Requests | THROTTLED | Implement exponential backoff |
| 500 Internal Error | INTERNAL_ERROR | Retry with backoff |
| 503 Service Unavailable | TRY_AGAIN_LATER | Temporary 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:
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:
FieldValueenum for type-safe field accessRecordInfostruct for read operationsQueryFilterfor building queriesCloudKitServicewrapper hiding OpenAPI complexity
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:
- I draft the structure
- Provide CloudKit domain knowledge and desired API
- Claude expands
- Fills in request/response schemas
- Generates boilerplate for similar endpoints
- Creates consistent patterns
- 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
- Claude validates consistency
- Catches schema mismatches
- Suggests improvements
- 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:
- Field Value with the oneOf pattern and handling the ASSETID quirk)
- completed authentication modeling with three security schemes
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:
- β 10,476 lines of generated, type-safe Swift code
- β Three authentication methods working seamlessly
- β CustomFieldValue handling CloudKit's polymorphic types
- β Clean public API hiding OpenAPI complexity
- β 161 tests across 47 test files
The OpenAPI spec was complete. The generated client compiled. The abstraction layer was elegant. Unit tests passed.
How Claude Code Accelerated Development:
- Documentation Translation: Converting Apple's prose documentation to a precise OpenAPI spec would have taken weeks manually. Claude handled the bulk of this in days, with me providing CloudKit domain expertise and corrections.
- Boilerplate Generation: The 10,476 lines of generated Swift code from swift-openapi-generator saved months of hand-writing networking code, request/response types, and JSON handling.
- Pattern Application: Once I established patterns (like
CustomFieldValuefor polymorphic types), Claude consistently applied them across the codebase. - Iteration Speed: When authentication approaches needed refactoring, Claude could update dozens of files in minutes vs. hours of manual editing.
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.