Chapter 14 — Conclusion and further reading

The layer you built

flowchart TB View["SwiftUI View"] --> VM["@Observable ViewModel
(Loadable state)"] VM --> Repo["Repository"] Repo --> Client["APIClient.send(Endpoint<T>)"] Client --> Pipe["Interceptors: API-key · auth (+refresh) · retry · logging · request-id"] Pipe --> Session["URLSession
(cache · metrics · pinning)"] Session --> API[(Chirp API)] Session -.->|"WebSocket"| WS[(live notifications)] Client -->|"decode Endpoint's Response"| VM Session -.->|"timing"| Obs["Sentry · DataDog · MetricsStore"]

The file tree you produced

Chirp/
├── App/
│   ├── ChirpApp.swift              (composition root: build client + interceptors)
│   ├── SessionManager.swift        (Ch 5 — signed-in/out switch)
│   └── RootView.swift              (Ch 5)
├── Networking/
│   ├── APIClient.swift             (Ch 3, 6, 7 — send + pipeline)
│   ├── Endpoint.swift  API.swift   (Ch 3 — typed endpoints)
│   ├── APIError.swift              (Ch 3–4)
│   ├── HTTPMethod.swift            (Ch 3)
│   ├── RequestInterceptor.swift    (Ch 7)
│   ├── AuthInterceptor.swift  APIKeyInterceptor.swift  RetryInterceptor.swift   (Ch 7)
│   ├── LoggingInterceptor.swift  RequestIDInterceptor.swift   (Ch 11)
│   ├── TokenProvider.swift         (Ch 6 — single-flight refresh actor)
│   ├── TokenStore.swift  KeychainTokenStore.swift  Keychain.swift   (Ch 5–6)
│   ├── Loadable.swift              (Ch 4)
│   ├── MultipartFormData.swift  ImageLoader.swift   (Ch 9)
│   ├── ChirpSocket.swift           (Ch 10 — WebSocket)
│   ├── MetricsCollector.swift  MetricsStore.swift   (Ch 12)
│   ├── PinningDelegate.swift        (Ch 13)
│   └── DTOs/                        (Ch 2 — models, Page, TokenResponse, APIErrorResponse)
├── Features/
│   ├── Auth/            (Ch 5)
│   ├── Feed/            (Ch 4, 8 — pagination)
│   ├── Profile/         (Ch 9 — avatar upload)
│   └── Notifications/   (Ch 10)
└── ChirpTests/         (Appendix D — URLProtocol stubs)

The principles the book taught

  1. A call is build → send → check status → decode. URLSession throws only for transport failures;
  2. Encode the request/response contract in types. A generic Endpoint<Response> lets the compiler
  3. Model UI state as a Loadable enum, not scattered booleans; reads are pessimistic, actions are
  4. Hide storage and services behind protocols (TokenStore, AuthRepository, EmailSender) — one
  5. Refresh tokens through an actor (single-flight) so concurrent 401s trigger one refresh; the whole
  6. Cross-cutting concerns are interceptors, composed in one place — auth, API key, retry, logging,
  7. Retry only transient, idempotent failures with backoff+jitter; cache with URLCache/ETag and
  8. Paginate by accumulating pages, prefetch early, dedup by id, keep the list on a load-more failure;
  9. Files need multipart uploads, progress, and multi-level image caching; big downloads stream to
  10. Use WebSockets for server push, with a receive loop, keepalive pings, and backoff reconnection —
  11. Observe production with logs + metrics + traces (Sentry + DataDog), redact secrets, and
  12. Pin only when the threat model earns it, prefer public-key pinning and declarative NSPinnedDomains,

A field guide to the mistakes you'll actually hit

Where to go from here

  • GraphQL / gRPC. Chirp is REST, but the same layered structure (typed operations, a client, state,
  • Offline-first & sync. Combine the caching here with a local store (Core Data / SwiftData — see the
  • Push notifications (APNs) for background delivery, complementing the foreground WebSocket.
  • Certificate transparency & advanced security, request signing, and mutual TLS for high-assurance
  • A Swift package. The Networking/ folder is app-agnostic — extract it into a reusable package and
  • The companion books. Vapor in Depth builds the Chirp server you've been calling — read them

A final word