refactor(api): extract the HTTP seam so endpoints can live in feature files - #38
Merged
Merged
Conversation
… files `APIClient.swift` was a 1808-line class whose transport helpers were all `private`. Swift's `private` is file-scoped, so an `extension APIClient` in any other file could not call `get`/`post`/`checkResponse` or read `baseURL` — which meant every new endpoint had to be appended to the one class body. Three concurrent workstreams doing that would conflict on every merge. - New `APIClientTransport.swift` holds the whole seam at `internal` access: the verb helpers, `checkResponse`, `perform`, raw-data reads/writes, and the multipart upload builder. `baseURL`, `session` and the three coders are now `internal let` on the class (read-only, documented as the seam) - A feature can now add `APIClient+<Feature>.swift` and compile. CLAUDE.md says so, next to the three-coders rule, and repeats the pbxproj registration step - New `delete` / `deleteDecoding` / `deleteCamel` collapse 20 hand-rolled DELETE request builders that differed only in path; `postMultipartRawData` collapses the 5 near-identical upload builders - That removes 21 force-unwraps from production paths (20 `.data(using:.utf8)!` across the multipart bodies, 2 in `deleteMessage`'s hand-built URL), which CLAUDE.md forbids. `deleteMessage` also stops re-implementing status handling and keeps only the 403 copy the bodyless route can't supply - `serverErrorMessage(from:)` replaces the reach into the private `ErrorResponse` wire type from the 409 conflict path Requests are byte-identical before and after — same headers, same bodies, same error mapping. `updateListSchemaStructured` (409), `pushDocumentSync` (429) and `patchScheduledMessage` (lenient decode) keep their hand-built requests because their status/decode handling is genuinely custom. Verified: build succeeds; 823 tests, 0 failures (iPhone 16 · 302E002E-…, -parallel-testing-enabled NO, E2E skipped) — unchanged from baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KD1sv3y8YWsDBiG31tUJWo
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Unblocks parallel work on
APIClient. The plan inwork-consolidation.md§4 has three threads adding endpoints at once, with the rule "each thread adds its endpoints in its ownAPIClient+<Feature>.swiftextension." That rule could not be followed as the code stood — every transport helper (get,post,postCamel,checkResponse, …) and every stored property (baseURL,session, the three coders) wasprivate, and Swift'sprivateis file-scoped. Anextension APIClientin another file simply would not compile, so every new endpoint had to be appended to one 1808-line class body — the exact merge conflict the rule was meant to prevent.This moves the seam to its own file at
internalaccess. Threads B and C can now openAPIClient+AI.swift/APIClient+Materialize.swiftand go.What's included
APIClientTransport.swift(new) — the complete HTTP seam atinternalaccess: verb helpers,perform,checkResponse, raw-data reads/writes, multipart upload.baseURL/session/decoder/encoder/camelCaseEncoderbecomeinternal leton the class, documented as the seam and read-only.CLAUDE.md— the convention, sitting next to the three-coders rule, including theproject.pbxprojregistration reminder (no synced groups).delete/deleteDecoding/deleteCamelcollapse 20 hand-rolled DELETE builders that differed only in path.postMultipartRawDatacollapses the 5 near-identical upload builders..data(using:.utf8)!in multipart bodies, 2 indeleteMessage's hand-built URL) — aCLAUDE.mdviolation.deleteMessagestops re-implementing status handling; it keeps only the 403 copy, which the bodyless route can't supply.serverErrorMessage(from:)replaces reaching into the privateErrorResponsewire type from the 409 path.APIClient.swift: 1808 → 1473 lines.Behavior
Requests are byte-identical before and after — same headers (including which verbs send
Content-Typevs.Acceptonly), same bodies, same error mapping. Three methods deliberately keep their hand-built requests because their handling is genuinely custom:updateListSchemaStructured(409 →.conflict),pushDocumentSync(429 →.rateLimited+Retry-After),patchScheduledMessage(lenient decode returningnil).Testing
xcodebuild build— BUILD SUCCEEDEDxcodebuild test -parallel-testing-enabled NO -skip-testing:InterlinedListTests/E2EReadOnlyTests— 823 tests, 0 failures, unchanged from the recorded baseline (iPhone 16 ·302E002E-…)Follow-ups
Next in the stack: D4 (avatar 405 — the live regression), G17 (lists shared with me), then the P1/P3 papercuts. D4's "drop the five force-unwraps at
APIClient.swift:224-229" is already done here, viapostMultipartRawData.🤖 Generated with Claude Code