URLSession networking with async/await

Sofia Martinez Jan 2026
1 tab
import Foundation

enum APIError: Error, LocalizedError {
    case invalidURL
    case invalidResponse
    case statusCode(Int)
    case decodingError(Error)
    case networkError(Error)

    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "Invalid URL"
        case .invalidResponse:
            return "Invalid response from server"
        case .statusCode(let code):
            return "Server returned status code \(code)"
        case .decodingError(let error):
            return "Failed to decode response: \(error.localizedDescription)"
        case .networkError(let error):
            return "Network error: \(error.localizedDescription)"
        }
    }
}

class APIService {
    static let shared = APIService()
    private let baseURL = "https://api.example.com"
    private let session: URLSession

    private init() {
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = 30
        configuration.waitsForConnectivity = true
        self.session = URLSession(configuration: configuration)
    }

    func fetchPosts() async throws -> [Post] {
        guard let url = URL(string: "\(baseURL)/posts") else {
            throw APIError.invalidURL
        }

        do {
            let (data, response) = try await session.data(from: url)

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

            guard (200...299).contains(httpResponse.statusCode) else {
                throw APIError.statusCode(httpResponse.statusCode)
            }

            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase

            do {
                let postsResponse = try decoder.decode(PostsResponse.self, from: data)
                return postsResponse.posts
            } catch {
                throw APIError.decodingError(error)
            }
        } catch let error as APIError {
            throw error
        } catch {
            throw APIError.networkError(error)
        }
    }

    func createPost(title: String, body: String) async throws -> Post {
        guard let url = URL(string: "\(baseURL)/posts") else {
            throw APIError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(AuthManager.shared.token ?? "")", forHTTPHeaderField: "Authorization")

        let postData = CreatePostRequest(title: title, body: body)
        request.httpBody = try JSONEncoder().encode(postData)

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

        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw APIError.invalidResponse
        }

        return try JSONDecoder().decode(Post.self, from: data)
    }
}

struct CreatePostRequest: Encodable {
    let title: String
    let body: String
}
1 file · swift Explain with highlit

Modern Swift networking uses async/await for cleaner asynchronous code compared to completion handlers. URLSession's async methods like data(from:) make network calls straightforward. I wrap API calls in a service layer with typed responses using Codable. Error handling uses try/catch blocks instead of nested closures. For request configuration, I create URLRequest objects with custom headers, HTTP methods, and body data. Response validation checks status codes before decoding. Async/await integrates seamlessly with SwiftUI—mark view methods with async and call with Task. This modern approach eliminates callback hell and makes async code read linearly like synchronous code.