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