Calling URLSession.shared.data(for:) from every feature repeats decisions about base URLs, authentication, encoding, errors, timeouts, and logging.
A shared client owns transport policy:
func request<Response: Decodable, Body: Encodable>(
path: String,
method: HTTPMethod,
body: Body?
) async throws(AppError) -> Response
The client obtains the current access token, creates a correlation ID, encodes snake-case or camel-case consistently, validates the HTTP response, unwraps the API envelope, and maps failures into AppError.
Feature API services remain small:
func loadReports() async throws(AppError) -> [ReportDTO] {
try await backend.request(path: "/api/v1/reports", method: .get)
}
Log method, route template, duration, status, response size, attempt, and correlation ID. Avoid raw authorization headers, request bodies, health values, and chat content.
Cancellation must propagate. Do not catch CancellationError and turn it into a generic offline failure. A cancelled search or abandoned screen is expected control flow.
Keep transport DTOs at the boundary and map them into domain models. That protects features when the API adds metadata or changes envelope details.
A networking layer should make the safe behavior automatic while still exposing policy choices such as timeout and retry. If every call requires ten flags, the abstraction has moved complexity rather than reduced it.