Skip to content

refactor(api): extract the HTTP seam so endpoints can live in feature files - #38

Merged
Adron merged 1 commit into
mainfrom
refactor/apiclient-transport-seam
Sep 6, 2026
Merged

refactor(api): extract the HTTP seam so endpoints can live in feature files#38
Adron merged 1 commit into
mainfrom
refactor/apiclient-transport-seam

Conversation

@Adron

@Adron Adron commented Sep 6, 2026

Copy link
Copy Markdown
Member

Summary

Unblocks parallel work on APIClient. The plan in work-consolidation.md §4 has three threads adding endpoints at once, with the rule "each thread adds its endpoints in its own APIClient+<Feature>.swift extension." 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) was private, and Swift's private is file-scoped. An extension APIClient in 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 internal access. Threads B and C can now open APIClient+AI.swift / APIClient+Materialize.swift and go.

What's included

  • APIClientTransport.swift (new) — the complete HTTP seam at internal access: verb helpers, perform, checkResponse, raw-data reads/writes, multipart upload. baseURL / session / decoder / encoder / camelCaseEncoder become internal let on the class, documented as the seam and read-only.
  • CLAUDE.md — the convention, sitting next to the three-coders rule, including the project.pbxproj registration reminder (no synced groups).
  • Dedupe that fell out of completing the seam:
    • delete / deleteDecoding / deleteCamel collapse 20 hand-rolled DELETE builders that differed only in path.
    • postMultipartRawData collapses the 5 near-identical upload builders.
    • Together those remove 21 force-unwraps from production paths (20 × .data(using:.utf8)! in multipart bodies, 2 in deleteMessage's hand-built URL) — a CLAUDE.md violation.
    • deleteMessage stops re-implementing status handling; it keeps only the 403 copy, which the bodyless route can't supply.
    • serverErrorMessage(from:) replaces reaching into the private ErrorResponse wire 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-Type vs. Accept only), 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 returning nil).

Testing

  • xcodebuild buildBUILD SUCCEEDED
  • xcodebuild test -parallel-testing-enabled NO -skip-testing:InterlinedListTests/E2EReadOnlyTests823 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, via postMultipartRawData.

🤖 Generated with Claude Code

… 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant