---
title: "Networking"
description: "URLSession patterns for API calls, authentication, caching, and offline support."
type: skill
canonical_url: https://claudary.paisolsolutions.com/skills/networking-3
source: "Claudary"
difficulty: intermediate
author: "Claude Code Knowledge Pack"
date: 2026-07-10T11:31:21.562Z
license: CC-BY-4.0
attribution: "Networking — Claudary (https://claudary.paisolsolutions.com/skills/networking-3)"
---

# Networking
URLSession patterns for API calls, authentication, caching, and offline support.

## Overview

# Networking

URLSession patterns for API calls, authentication, caching, and offline support.

<basic_requests>
<async_await>
```swift
actor NetworkService {
    private let session: URLSession
    private let decoder: JSONDecoder

    init(session: URLSession = .shared) {
        self.session = session
        self.decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
    }

    func fetch<T: Decodable>(_ request: URLRequest) async throws -> T {
        let (data, response) = try await session.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw NetworkError.invalidResponse
        }

        guard 200..<300 ~= httpResponse.statusCode else {
            throw NetworkError.httpError(httpResponse.statusCode, data)
        }

        return try decoder.decode(T.self, from: data)
    }

    func fetchData(_ request: URLRequest) async throws -> Data {
        let (data, response) = try await session.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse,
              200..<300 ~= httpResponse.statusCode else {
            throw NetworkError.requestFailed
        }

        return data
    }
}

enum NetworkError: Error {
    case invalidResponse
    case httpError(Int, Data)
    case requestFailed
    case decodingError(Error)
}
```
</async_await>

<request_building>
```swift
struct Endpoint {
    let path: String
    let method: HTTPMethod
    let queryItems: [URLQueryItem]?
    let body: Data?
    let headers: [String: String]?

    enum HTTPMethod: String {
        case get = "GET"
        case post = "POST"
        case put = "PUT"
        case patch = "PATCH"
        case delete = "DELETE"
    }

    var request: URLRequest {
        var components = URLComponents()
        components.scheme = "https"
        components.host = "api.example.com"
        components.path = path
        components.queryItems = queryItems

        var request = URLRequest(url: components.url!)
        request.httpMethod = method.rawValue
        request.httpBody = body

        // Default headers
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")

        // Custom headers
        headers?.forEach { request.setValue($1, forHTTPHeaderField: $0) }

        return request
    }
}

// Usage
extension Endpoint {
    static func projects() -> Endpoint {
        Endpoint(path: "/v1/projects", method: .get, queryItems: nil, body: nil, headers: nil)
    }

    static func project(id: UUID) -> Endpoint {
        Endpoint(path: "/v1/projects/\\(id)", method: .get, queryItems: nil, body: nil, headers: nil)
    }

    static func createProject(_ project: CreateProjectRequest) -> Endpoint {
        let body = try? JSONEncoder().encode(project)
        return Endpoint(path: "/v1/projects", method: .post, queryItems: nil, body: body, headers: nil)
    }
}
```
</request_building>
</basic_requests>

<authentication>
<bearer_token>
```swift
actor AuthenticatedNetworkService {
    private let session: URLSession
    private var token: String?

    init() {
        let config = URLSessionConfiguration.default
        config.httpAdditionalHeaders = [
            "User-Agent": "MyApp/1.0"
        ]
        self.session = URLSession(configuration: config)
    }

    func setToken(_ token: String) {
        self.token = token
    }

    func fetch<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
        var request = endpoint.request

        if let token = token {
            request.setValue("Bearer \\(token)", forHTTPHeaderField: "Authorization")
        }

        let (data, response) = try await session.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw NetworkError.invalidResponse
        }

        if httpResponse.statusCode == 401 {
            throw NetworkError.unauthorized
        }

        guard 200..<300 ~= httpResponse.statusCode else {
            throw NetworkError.httpError(httpResponse.statusCode, data)
        }

        return try JSONDecoder().decode(T.self, from: data)
    }
}
```
</bearer_token>

<oauth_refresh>
```swift
actor OAuthService {
    private var accessToken: String?
    private var refreshToken: String?
    private var tokenExpiry: Date?
    private var isRefreshing = false

    func validToken() async throws -> String {
        // Return existing valid token
        if let token = accessToken,
           let expiry = tokenExpiry,
           expiry > Date().addingTimeInterval(60) {
            return token
        }

        // Refresh if needed
        return try await refreshAccessToken()
    }

    private func refreshAccessToken() async throws -> String {
        guard !isRefreshing else {
            // Wait for in-progress refresh
            try await Task.sleep(for: .milliseconds(100))
            return try await validToken()
        }

        isRefreshing = true
        defer { isRefreshing = false }

        guard let refresh = refreshToken else {
            throw AuthError.noRefreshToken
        }

        let request = Endpoint.refreshToken(refresh).request
        let (data, _) = try await URLSession.shared.data(for: request)
        let response = try JSONDecoder().decode(TokenResponse.self, from: data)

        accessToken = response.accessToken
        refreshToken = response.refreshToken
        tokenExpiry = Date().addingTimeInterval(TimeInterval(response.expiresIn))

        // Save to keychain
        try saveTokens()

        return response.accessToken
    }
}
```
</oauth_refresh>
</authentication>

<caching>
<urlcache>
```swift
// Configure cache in URLSession
let config = URLSessionConfiguration.default
config.urlCache = URLCache(
    memoryCapacity: 50 * 1024 * 1024,  // 50 MB memory
    diskCapacity: 100 * 1024 * 1024,   // 100 MB disk
    diskPath: "network_cache"
)
config.requestCachePolicy = .returnCacheDataElseLoad

let session = URLSession(configuration: config)
```
</urlcache>

<custom_cache>
```swift
actor ResponseCache {
    private var cache: [String: CachedResponse] = [:]
    private let maxAge: TimeInterval

    init(maxAge: TimeInterval = 300) {  // 5 minutes default
        self.maxAge = maxAge
    }

    func get<T: Decodable>(_ key: String) -> T? {
        guard let cached = cache[key],
              Date().timeIntervalSince(cached.timestamp) < maxAge else {
            cache[key] = nil
            return nil
        }

        return try? JSONDecoder().decode(T.self, from: cached.data)
    }

    func set<T: Encodable>(_ value: T, for key: String) {
        guard let data = try? JSONEncoder().encode(value) else { return }
        cache[key] = CachedResponse(data: data, timestamp: Date())
    }

    func invalidate(_ key: String) {
        cache[key] = nil
    }

    func clear() {
        cache.removeAll()
    }
}

struct CachedResponse {
    let data: Data
    let timestamp: Date
}

// Usage
actor CachedNetworkService {
    private let network: NetworkService
    private let cache = ResponseCache()

    func fetchProjects(forceRefresh: Bool = false) async throws -> [Project] {
        let cacheKey = "projects"

        if !forceRefresh, let cached: [Project] = await cache.get(cacheKey) {
            return cached
        }

        let projects: [Project] = try await network.fetch(Endpoint.projects().request)
        await cache.set(projects, for: cacheKey)

        return projects
    }
}
```
</custom_cache>
</caching>

<offline_support>
```swift
@Observable
class OfflineAwareService {
    private let network: NetworkService
    private let storage: LocalStorage
    var isOnline = true

    init(network: NetworkService, storage: LocalStorage) {
        self.network = network
        self.storage = storage
        monitorConnectivity()
    }

    func fetchProjects() async throws -> [Project] {
        if isOnline {
            do {
                let projects = try await network.fetch(Endpoint.projects().request)
                try storage.save(projects, for: "projects")
                return projects
            } catch {
                // Fall back to cache on network error
                if let cached = try? storage.load("projects") as [Project] {
                    return cached
                }
                throw error
            }
        } else {
            // Offline: use cache
            guard let cached = try? storage.load("projects") as [Project] else {
                throw NetworkError.offline
            }
            return cached
        }
    }

    private func monitorConnectivity() {
        let monitor = NWPathMonitor()
        monitor.pathUpdateHandler = { [weak self] path in
            Task { @MainActor in
                self?.isOnline = path.status == .satisfied
            }
        }
        monitor.start(queue: .global())
    }
}
```
</offline_support>

<upload_download>
<file_upload>
```swift
actor UploadService {
    func upload(file: URL, to endpoint: Endpoint) async throws -> UploadResponse {
        var request = endpoint.request

        let boundary = UUID().uuidString
        request.setValue("multipart/form-data; boundary=\\(boundary)", forHTTPHeaderField: "Content-Type")

        let data = try Data(contentsOf: file)
        let body = createMultipartBody(
            data: data,
            filename: file.lastPathComponent,
            boundary: boundary
        )
        request.httpBody = body

        let (responseData, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(UploadResponse.self, from: responseData)
    }

    private func createMultipartBody(data: Data, filename: String, boundary: String) -> Data {
        var body = Data()

        body.append("--\\(boundary)\\r\\n".data(using: .utf8)!)
        body.append("Content-Disposition: form-data; name=\\"file\\"; filename=\\"\\(filename)\\"\\r\\n".data(using: .utf8)!)
        body.append("Content-Type: application/octet-stream\\r\\n\\r\\n".data(using: .utf8)!)
        body.append(data)
        body.append("\\r\\n--\\(boundary)--\\r\\n".data(using: .utf8)!)

        return body
    }
}
```
</file_upload>

<file_download>
```swift
actor DownloadService {
    func download(from url: URL, to destination: URL) async throws {
        let (tempURL, response) = try await URLSession.shared.download(from: url)

        guard let httpResponse = response as? HTTPURLResponse,
              200..<300 ~= httpResponse.statusCode else {
            throw NetworkError.downloadFailed
        }

        // Move to destination
        let fileManager = FileManager.default
        if fileManager.fileExists(atPath: destination.path) {
            try fileManager.removeItem(at: destination)
        }
        try fileManager.moveItem(at: tempURL, to: destination)
    }

    func downloadWithProgress(from url: URL) -> AsyncThrowingStream<DownloadProgress, Error> {
        AsyncThrowingStream { continuation in
            let task = URLSession.shared.downloadTask(with: url) { tempURL, response, error in
                if let error = error {
                    continuation.finish(throwing: error)
                    return
                }

                guard let tempURL = tempURL else {
                    continuation.finish(throwing: NetworkError.downloadFailed)
                    return
                }

                continuation.yield(.completed(tempURL))
                continuation.finish()
            }

            // Observe progress
            let observation = task.progress.observe(\\.fractionCompleted) { progress, _ in
                continuation.yield(.progress(progress.fractionCompleted))
            }

            continuation.onTermination = { _ in
                observation.invalidate()
                task.cancel()
            }

            task.resume()
        }
    }
}

enum DownloadProgress {
    case progress(Double)
    case completed(URL)
}
```
</file_download>
</upload_download>

<error_handling>
```swift
enum NetworkError: LocalizedError {
    case invalidResponse
    case httpError(Int, Data)
    case unauthorized
    case offline
    case timeout
    case decodingError(Error)

    var errorDescription: String? {
        switch self {
        case .invalidResponse:
            return "Invalid server response"
        case .httpError(let code, _):
            return "Server error: \\(code)"
        case .unauthorized:
            return "Authentication required"
        case .offline:
            return "No internet connection"
        case .timeout:
            return "Request timed out"
        case .decodingError(let error):
            return "Data error: \\(error.localizedDescription)"
        }
    }

    var isRetryable: Bool {
        switch self {
        case .httpError(let code, _):
            return code >= 500
        case .timeout, .offline:
            return true
        default:
            return false
        }
    }
}

// Retry logic
func fetchWithRetry<T: Decodable>(
    _ request: URLRequest,
    maxAttempts: Int = 3
) async throws -> T {
    var lastError: Error?

    for attempt in 1...maxAttempts {
        do {
            return try await network.fetch(request)
        } catch let error as NetworkError where error.isRetryable {
            lastError = error
            let delay = pow(2.0, Double(attempt - 1))  // Exponential backoff
            try await Task.sleep(for: .seconds(delay))
        } catch {
            throw error
        }
    }

    throw lastError ?? NetworkError.requestFailed
}
```
</error_handling>

<testing>
```swift
// Mock URLProtocol for testing
class MockURLProtocol: URLProtocol {
    static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?

    override class func canInit(with request: URLRequest) -> Bool {
        true
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        request
    }

    override func startLoading() {
        guard let handler = MockURLProtocol.requestHandler else {
            fatalError("Handler not set")
        }

        do {
            let (response, data) = try handler(request)
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        } catch {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }

    override func stopLoading() {}
}

// Test setup
func testFetchProjects() async throws {
    let config = URLSessionConfiguration.ephemeral
    config.protocolClasses = [MockURLProtocol.self]
    let session = URLSession(configuration: config)

    MockURLProtocol.requestHandler = { request in
        let response = HTTPURLResponse(
            url: request.url!,
            statusCode: 200,
            httpVersion: nil,
            headerFields: nil
        )!

---

Source: [Claudary](https://claudary.paisolsolutions.com/skills/networking-3) · https://claudary.paisolsolutions.com
