Introduction
Modern iOS and macOS applications rarely function without interacting with remote servers. Whether it's loading data from a news feed, submitting a registration form, or synchronizing files — all these operations require a well-implemented HTTP client. In the Swift ecosystem, developers have access to the powerful built-in framework URLSession, which becomes more convenient every year thanks to the introduction of async/await.
In this article, we will explore how to properly create HTTP requests in Swift, starting from basic structures and ending with advanced techniques for error handling and data serialization. You will learn how to write clean, safe, and performant networking code for your applications.
URLSession Basics: Your First HTTP Request
The URLSession class is the foundation for any networking work in Swift. It manages configuration, caching, sessions, and the direct execution of requests. Let's look at a simple GET request to fetch data from a server.
Creating a Simple GET Request
import Foundation
// 1. Create a URLguard let url = URL(string: "https://api.example.com/users") else { print("Invalid URL") return}
// 2. Create a request (GET by default)var request = URLRequest(url: url)request.httpMethod = "GET"request.setValue("application/json", forHTTPHeaderField: "Accept")
// 3. Create a sessionlet session = URLSession.shared
// 4. Execute the request (old way with closure)let task = session.dataTask(with: request) { data, response, error in if let error = error { print("Request error: \\(error.localizedDescription)") return } guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { print("Server returned an error") return } if let data = data, let jsonString = String(data: data, encoding: .utf8) { print("Received data: \\(jsonString)") }}task.resume() // Important! Start the taskKey points: always check the response status (200...299), don't forget to call resume(), and handle potential errors. In real projects, this code is usually extracted into a separate service layer.
Modern Approach: async/await
Starting with Swift 5.5, it became possible to use async/await, which makes the code much more readable and eliminates the "pyramid of closures".
func fetchUsers() async throws -> Data { guard let url = URL(string: "https://api.example.com/users") else { throw URLError(.badURL) } var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("application/json", forHTTPHeaderField: "Accept") let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { throw URLError(.badServerResponse) } return data}This code can be called from any asynchronous function or task, significantly simplifying the logic.
Working with JSON: Codable and Data Models
Data rarely comes in "raw" form. Most often, we deal with JSON. Swift offers an elegant solution — the Codable protocol (combining Encodable and Decodable).
Creating a Model and Decoding
// 1. Define the data structurestruct User: Codable { let id: Int let name: String let email: String}
// 2. Asynchronous function to fetch an array of usersfunc fetchUsers() async throws -> [User] { let data = try await fetchUsersData() // from the previous example let decoder = JSONDecoder() return try decoder.decode