Introduction
Making REST API calls in Swift uses URLSession, Apple's built-in networking framework. It supports all HTTP methods (GET, POST, PUT, DELETE), handles JSON encoding/decoding, and works asynchronously to keep the UI responsive. Swift 5.5+ also supports async/await for cleaner networking code.
HTTP Methods
GET: Retrieve data from a server
POST: Send data to a server to create a resource
PUT: Update a resource on a server
DELETE: Remove a resource from a server
GET Request
Completion Handler (Traditional)
1func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) {
2 let url = URL(string: "https://api.example.com/users")!
3
4 URLSession.shared.dataTask(with: url) { data, response, error in
5 if let error = error {
6 completion(.failure(error))
7 return
8 }
9
10 guard let data = data else {
11 completion(.failure(URLError(.badServerResponse)))
12 return
13 }
14
15 do {
16 let users = try JSONDecoder().decode([User].self, from: data)
17 completion(.success(users))
18 } catch {
19 completion(.failure(error))
20 }
21 }.resume()
22}
Async/Await (Swift 5.5+)
1func fetchUsers() async throws -> [User] {
2 let url = URL(string: "https://api.example.com/users")!
3 let (data, response) = try await URLSession.shared.data(from: url)
4
5 guard let httpResponse = response as? HTTPURLResponse,
6 httpResponse.statusCode == 200 else {
7 throw URLError(.badServerResponse)
8 }
9
10 return try JSONDecoder().decode([User].self, from: data)
11}
12
13// Usage
14Task {
15 do {
16 let users = try await fetchUsers()
17 print(users)
18 } catch {
19 print("Error: \(error)")
20 }
21}
POST Request
1struct CreateUserRequest: Encodable {
2 let name: String
3 let email: String
4}
5
6func createUser(name: String, email: String) async throws -> User {
7 let url = URL(string: "https://api.example.com/users")!
8 var request = URLRequest(url: url)
9 request.httpMethod = "POST"
10 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
11
12 let body = CreateUserRequest(name: name, email: email)
13 request.httpBody = try JSONEncoder().encode(body)
14
15 let (data, response) = try await URLSession.shared.data(for: request)
16
17 guard let httpResponse = response as? HTTPURLResponse,
18 (200...299).contains(httpResponse.statusCode) else {
19 throw URLError(.badServerResponse)
20 }
21
22 return try JSONDecoder().decode(User.self, from: data)
23}
PUT and DELETE Requests
1// PUT - Update a resource
2func updateUser(id: Int, name: String) async throws -> User {
3 let url = URL(string: "https://api.example.com/users/\(id)")!
4 var request = URLRequest(url: url)
5 request.httpMethod = "PUT"
6 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
7 request.httpBody = try JSONEncoder().encode(["name": name])
8
9 let (data, _) = try await URLSession.shared.data(for: request)
10 return try JSONDecoder().decode(User.self, from: data)
11}
12
13// DELETE - Remove a resource
14func deleteUser(id: Int) async throws {
15 let url = URL(string: "https://api.example.com/users/\(id)")!
16 var request = URLRequest(url: url)
17 request.httpMethod = "DELETE"
18
19 let (_, response) = try await URLSession.shared.data(for: request)
20 guard let httpResponse = response as? HTTPURLResponse,
21 httpResponse.statusCode == 204 else {
22 throw URLError(.badServerResponse)
23 }
24}
Codable Models
Define models that map to your JSON:
1struct User: Codable {
2 let id: Int
3 let name: String
4 let email: String
5 let createdAt: String
6
7 enum CodingKeys: String, CodingKey {
8 case id, name, email
9 case createdAt = "created_at"
10 }
11}
Adding Headers and Authentication
1var request = URLRequest(url: url)
2request.setValue("application/json", forHTTPHeaderField: "Content-Type")
3request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
4request.setValue("gzip", forHTTPHeaderField: "Accept-Encoding")
Generic Network Layer
1class APIClient {
2 static let shared = APIClient()
3 private let session = URLSession.shared
4 private let baseURL = "https://api.example.com"
5
6 func request<T: Decodable>(
7 path: String,
8 method: String = "GET",
9 body: Encodable? = nil
10 ) async throws -> T {
11 let url = URL(string: "\(baseURL)\(path)")!
12 var request = URLRequest(url: url)
13 request.httpMethod = method
14 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
15
16 if let body = body {
17 request.httpBody = try JSONEncoder().encode(body)
18 }
19
20 let (data, response) = try await session.data(for: request)
21
22 guard let httpResponse = response as? HTTPURLResponse,
23 (200...299).contains(httpResponse.statusCode) else {
24 throw URLError(.badServerResponse)
25 }
26
27 let decoder = JSONDecoder()
28 decoder.keyDecodingStrategy = .convertFromSnakeCase
29 return try decoder.decode(T.self, from: data)
30 }
31}
32
33// Usage
34let users: [User] = try await APIClient.shared.request(path: "/users")
Common Pitfalls
Main thread updates: URLSession callbacks run on a background thread. Update UI on the main thread: DispatchQueue.main.async { } or use @MainActor.
ATS (App Transport Security): iOS blocks non-HTTPS connections by default. Add exceptions in Info.plist if needed, or preferably use HTTPS.
Forgetting .resume(): URLSession.dataTask() returns a suspended task. Call .resume() to start it. The async/await API does not require this.
JSON key mismatch: Swift uses camelCase; APIs often use snake_case. Use keyDecodingStrategy = .convertFromSnakeCase or CodingKeys enum.
Retain cycles: In completion handler closures, capture [weak self] to avoid retain cycles in view controllers.
Summary
Use URLSession.shared.data(from:) with async/await for clean networking code
Configure URLRequest with HTTP method, headers, and body for POST/PUT/DELETE
Use Codable for automatic JSON encoding/decoding
Always handle errors and check HTTP status codes
Update UI on the main thread after network responses