Swift
REST API
Networking
iOS Development
HTTP Requests

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

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

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

swift
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

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

Adding Authentication Headers

swift
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

swift
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

swift
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

swift
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 MethodUse CaseStatus Code
GETFetch data200
POSTCreate resource201
PUTUpdate resource200
DELETERemove resource204
  • 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

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.