Introduction
Making REST API calls in Swift is done using URLSession, Apple's built-in networking framework. It handles HTTP requests, response parsing, authentication, and background downloads without requiring any third-party libraries. This article covers GET, POST, PUT, and DELETE requests with proper error handling and JSON parsing using Codable.
Basic GET Request
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
13struct User: Codable {
14 let id: Int
15 let name: String
16 let email: String
17}
Validating the Response
Every API response should be checked for three things:
Data: Is there data returned from the server?
HTTP Status Code: Does the server respond with a success code (200-299)?
Error: Is there a network error (timeout, no connection)?
1func fetchData(from urlString: String) async throws -> Data {
2 guard let url = URL(string: urlString) else {
3 throw URLError(.badURL)
4 }
5
6 let (data, response) = try await URLSession.shared.data(from: url)
7
8 guard let httpResponse = response as? HTTPURLResponse else {
9 throw URLError(.badServerResponse)
10 }
11
12 guard (200...299).contains(httpResponse.statusCode) else {
13 throw URLError(.badServerResponse)
14 }
15
16 return data
17}
POST Request
Send JSON data to create a resource:
1func createUser(name: String, email: String) async throws -> User {
2 let url = URL(string: "https://api.example.com/users")!
3
4 var request = URLRequest(url: url)
5 request.httpMethod = "POST"
6 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
7
8 let body = ["name": name, "email": email]
9 request.httpBody = try JSONEncoder().encode(body)
10
11 let (data, response) = try await URLSession.shared.data(for: request)
12
13 guard let httpResponse = response as? HTTPURLResponse,
14 httpResponse.statusCode == 201 else {
15 throw URLError(.badServerResponse)
16 }
17
18 return try JSONDecoder().decode(User.self, from: data)
19}
PUT Request
Update an existing resource:
1func updateUser(id: Int, name: String) async throws -> User {
2 let url = URL(string: "https://api.example.com/users/\(id)")!
3
4 var request = URLRequest(url: url)
5 request.httpMethod = "PUT"
6 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
7
8 let body = ["name": name]
9 request.httpBody = try JSONEncoder().encode(body)
10
11 let (data, _) = try await URLSession.shared.data(for: request)
12 return try JSONDecoder().decode(User.self, from: data)
13}
DELETE Request
1func deleteUser(id: Int) async throws {
2 let url = URL(string: "https://api.example.com/users/\(id)")!
3
4 var request = URLRequest(url: url)
5 request.httpMethod = "DELETE"
6
7 let (_, response) = try await URLSession.shared.data(for: request)
8
9 guard let httpResponse = response as? HTTPURLResponse,
10 httpResponse.statusCode == 204 else {
11 throw URLError(.badServerResponse)
12 }
13}
1func fetchProtectedData() async throws -> Data {
2 let url = URL(string: "https://api.example.com/profile")!
3 var request = URLRequest(url: url)
4
5 // Bearer token
6 request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
7
8 // API key
9 request.setValue("your-api-key", forHTTPHeaderField: "X-API-Key")
10
11 let (data, _) = try await URLSession.shared.data(for: request)
12 return data
13}
Query Parameters
1func searchUsers(query: String, page: Int) async throws -> [User] {
2 var components = URLComponents(string: "https://api.example.com/users")!
3 components.queryItems = [
4 URLQueryItem(name: "q", value: query),
5 URLQueryItem(name: "page", value: String(page)),
6 URLQueryItem(name: "limit", value: "20")
7 ]
8
9 let url = components.url! // https://api.example.com/users?q=john&page=1&limit=20
10 let (data, _) = try await URLSession.shared.data(from: url)
11 return try JSONDecoder().decode([User].self, from: data)
12}
Building a Reusable API Client
1class APIClient {
2 static let shared = APIClient()
3 private let session = URLSession.shared
4 private let baseURL = "https://api.example.com"
5 private let decoder: JSONDecoder = {
6 let decoder = JSONDecoder()
7 decoder.keyDecodingStrategy = .convertFromSnakeCase
8 decoder.dateDecodingStrategy = .iso8601
9 return decoder
10 }()
11
12 func request<T: Decodable>(
13 path: String,
14 method: String = "GET",
15 body: Encodable? = nil
16 ) async throws -> T {
17 let url = URL(string: "\(baseURL)\(path)")!
18 var request = URLRequest(url: url)
19 request.httpMethod = method
20 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
21
22 if let body = body {
23 request.httpBody = try JSONEncoder().encode(body)
24 }
25
26 let (data, response) = try await session.data(for: request)
27
28 guard let httpResponse = response as? HTTPURLResponse,
29 (200...299).contains(httpResponse.statusCode) else {
30 throw URLError(.badServerResponse)
31 }
32
33 return try decoder.decode(T.self, from: data)
34 }
35}
36
37// Usage
38let users: [User] = try await APIClient.shared.request(path: "/users")
39let newUser: User = try await APIClient.shared.request(
40 path: "/users",
41 method: "POST",
42 body: CreateUserRequest(name: "John", email: "[email protected]")
43)
Calling from SwiftUI
1struct UsersView: View {
2 @State private var users: [User] = []
3 @State private var isLoading = true
4 @State private var error: String?
5
6 var body: some View {
7 List(users, id: \.id) { user in
8 Text(user.name)
9 }
10 .task {
11 do {
12 users = try await fetchUsers()
13 isLoading = false
14 } catch {
15 self.error = error.localizedDescription
16 isLoading = false
17 }
18 }
19 }
20}
Common Pitfalls
Main thread updates: URLSession callbacks run on a background thread. In SwiftUI, @State updates are automatically dispatched to the main thread, but in UIKit you must use DispatchQueue.main.async to update UI.
App Transport Security (ATS): iOS blocks HTTP (non-HTTPS) requests by default. For development with local servers, add an ATS exception in Info.plist, but always use HTTPS in production.
JSON key naming: APIs often use snake_case (user_name) while Swift uses camelCase (userName). Set decoder.keyDecodingStrategy = .convertFromSnakeCase to handle this automatically.
Timeout configuration: The default timeout is 60 seconds. For long-running requests, configure URLRequest.timeoutInterval or the session's URLSessionConfiguration.
Retain cycles with closures: When using completion handler-based APIs (pre-async/await), use [weak self] in closures to avoid retain cycles in view controllers.
Summary
| HTTP Method | Use Case | Status Code |
| GET | Fetch data | 200 |
| POST | Create resource | 201 |
| PUT | Update resource | 200 |
| DELETE | Remove resource | 204 |
Use URLSession.shared.data(from:) for GET, data(for:) for other methods
Parse JSON with JSONDecoder and Codable models
Always validate HTTP status codes (200-299 range)
Use async/await (iOS 15+) for clean, readable networking code
Build a reusable APIClient to avoid duplication across your app