HTTP Client in Swift: Making Requests the Right Way

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why a Proper HTTP Client Matters for Your Swift App

Modern iOS and macOS apps rarely function without interacting with remote servers. Whether it's loading data from a REST API, sending analytics, or working with cloud services — an HTTP client in Swift is the foundation of network communication. However, many developers, especially beginners, make typical mistakes: ignoring error handling, not using modern language features (async/await), or creating "spaghetti code" from network calls. In this article, we'll break down how to write a reliable and performant HTTP client in Swift, following best practices.



1. URLSession Basics: The Heart of Network Requests

In the Swift standard library, the Foundation framework and the URLSession class handle network communication. This is a powerful tool that supports session configuration, caching, background requests, and much more.



1.1. Creating a Basic HTTP Client

Let's start with a simple structure that will perform GET requests. We'll use an enum to describe possible errors and a protocol for flexibility:

import Foundation

enum HTTPClientError: Error { case invalidURL case noData case decodingError(Error) case networkError(Error) case statusCode(Int)}

protocol HTTPClientProtocol { func get<T: Decodable>(from urlString: String, responseType: T.Type) async throws -> T}

final class HTTPClient: HTTPClientProtocol { private let session: URLSession private let decoder: JSONDecoder init(session: URLSession = .shared, decoder: JSONDecoder = JSONDecoder()) { self.session = session self.decoder = decoder } func get<T: Decodable>(from urlString: String, responseType: T.Type) async throws -> T { guard let url = URL(string: urlString) else { throw HTTPClientError.invalidURL } var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("application/json", forHTTPHeaderField: "Accept") let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw HTTPClientError.networkError(NSError(domain: "Invalid response", code: -1)) } guard (200...299).contains(httpResponse.statusCode) else { throw HTTPClientError.statusCode(httpResponse.statusCode) } do { return try decoder.decode(T.self, from: data) } catch { throw HTTPClientError.decodingError(error) } }}


1.2. Why async/await?

Before Swift 5.5, developers used closures (completion handlers) or third-party libraries (Alamofire). Async/await makes code linear, clear, and easy to read. Errors are handled through do-catch instead of nested closures.



2. POST, PUT, DELETE: Working with Different HTTP Methods

A real HTTP client should support all major methods. Let's add methods to the protocol for sending data and updating resources.



2.1. Extending the Protocol for POST and PUT

extension HTTPClientProtocol {    func post<T: Decodable, U: Encodable>(        to urlString: String,        body: U,        responseType: T.Type    ) async throws -> T {        guard let url = URL(string: urlString) else {            throw HTTPClientError.invalidURL        }                var request = URLRequest(url: url)        request.httpMethod = "POST"        request.setValue("application/json", forHTTPHeaderField: "Content-Type")        request.setValue("application/json", forHTTPHeaderField: "Accept")        request.httpBody = try JSONEncoder().encode(body)                return try a

Blogs

Book Recommendations