Swift
REST API
Networking
API Call
iOS Development

Make REST API call in Swift

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

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)

swift
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+)

swift
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

swift
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

swift
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:

swift
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

swift
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

swift
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

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.