Learn Swift - Networking & API Integration
Episode 8 of 23

Learn Swift - Networking & API Integration

This episode covers networking in Swift: making HTTP requests with URLSession, parsing JSON responses using Codable complete with error handling, using async/await for clean code, and best practices such as caching, retries, and request timeouts.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Modern apps rarely stand alone — they communicate with servers to fetch data, send input, and keep state synchronized. Episode 8 covers networking and API integration in Swift: from HTTP requests with URLSession, to JSON parsing with Codable, up to modern concurrency with async/await.

By the end of this episode you'll build a secure, testable, production-ready API client — including error handling, timeouts, and retry. Networking is a topic where small quality details, like clean error handling, make a huge difference in real applications.

URLSession and HTTP Requests

Making a Basic Request

URLSession is the foundation of networking on Apple platforms. Combined with async/await, it becomes very concise:

GET request with async/await
import Foundation
 
let url = URL(string: "https://api.github.com/users/arman")!
 
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
 
let (data, response) = try await URLSession.shared.data(for: request)
print("Bytes diterima: \(data.count)")

try await URLSession.shared.data(for: request) suspends the function until the response arrives, without blocking a thread. URLSession.shared is the default session, sufficient for most needs; we'll build a custom session with configuration for caching and timeouts below.

Checking the Response Status

URLResponse must be inspected — a successful data transfer doesn't mean a successful HTTP response. The recommended pattern: cast to HTTPURLResponse and check the status code:

Validate the status code
guard let http = response as? HTTPURLResponse,
      (200..<300).contains(http.statusCode) else {
    throw ErrorJaringan.statusBuruk(code: (response as? HTTPURLResponse)?.statusCode ?? -1)
}
print("Status OK: \(http.statusCode)")

The guard ... else { throw ... } pattern separates the error path early so the function stays short and clear. Treat 4xx as client-side errors and 5xx as server failures — each needs a different response.

Parsing JSON with Codable

Model and Decoder

Raw JSON data is useless until mapped to Swift types. Episode 7 introduced Codable; now we apply it to API responses:

Decode the JSON response
struct Pengguna: Codable {
    let login: String
    let name: String
    let publicRepos: Int
 
    enum CodingKeys: String, CodingKey {
        case login
        case name
        case publicRepos = "public_repos"
    }
}
 
let pengguna = try JSONDecoder().decode(Pengguna.self, from: data)
print(pengguna.login)

try JSONDecoder().decode(Pengguna.self, from: data) turns Data into a Pengguna instance, throwing a decoding error if the structure doesn't match. Use CodingKeys to adapt the API's snake_case to Swift's camelCase.

Response Error Handling

Combine the whole process into a single function that returns a value or throws an error:

Complete fetch function
func ambilPengguna(login: String) async throws -> Pengguna {
    let url = URL(string: "https://api.github.com/users/\(login)")!
    let (data, response) = try await URLSession.shared.data(from: url)
 
    guard let http = response as? HTTPURLResponse else {
        throw ErrorJaringan.tidakAdaKoneksi
    }
    guard (200..<300).contains(http.statusCode) else {
        throw ErrorJaringan.statusBuruk(code: http.statusCode)
    }
    return try JSONDecoder().decode(Pengguna.self, from: data)
}

func ambilPengguna(login: String) async throws -> Pengguna captures every possible failure: network, HTTP status, and decoding. Callers only need to write a single-level do-catch — a pattern that keeps callers simple.

Session Configuration and Timeouts

URLSessionConfiguration

For production apps, configure the session explicitly instead of using the shared session:

Session with timeout
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
config.waitsForConnectivity = true
config.urlCache = URLCache(memoryCapacity: 20_000_000,
                           diskCapacity: 50_000_000)
 
let session = URLSession(configuration: config)

config.timeoutIntervalForRequest = 30 limits how long to wait for a response per request. waitsForConnectivity = true makes the session wait for the network to recover instead of failing immediately. A session with this configuration is worth sharing as a singleton in your app.

Caching and Retries

Cache Strategies

URLSession integrates URLCache transparently with HTTP headers like Cache-Control. For data that rarely changes, combine it with a policy:

Check the API cache header
curl -sI https://api.github.com/users/arman | grep -i cache-control

curl -sI ... | grep -i cache-control shows the cache directives the server sends. Respect these directives — forcing cached responses without considering server headers can serve stale data.

Retry with Exponential Backoff

Networks aren't always stable. A simple retry with escalating delays improves resilience without overloading the server:

Retry with backoff
func cobaUlang<T>(_ operasi: () async throws -> T) async throws -> T {
    var penundaan: UInt64 = 500_000_000
    for _ in 0..<3 {
        do {
            return try await operasi()
        } catch {
            try await Task.sleep(nanoseconds: penundaan)
            penundaan *= 2
        }
    }
    return try await operasi()
}

try await Task.sleep(nanoseconds: penundaan) pauses execution without blocking a thread. cobaUlang retries the request with delays of 0.5 then 1 second, up to three attempts. Limit the number of retries — uncontrolled retries only add load when the server is already struggling.

Warning

Never use ! to force-unwrap a URL built from user input. Use validation and safe fallbacks; a URL that fails to construct should be an error, not a crash.

Closing

Key takeaways:

  • URLSession with async/await makes HTTP requests concise and non-blocking.
  • Always check the status code before processing data.
  • JSONDecoder maps JSON to Codable types; CodingKeys handles name differences.
  • Wrap all networking failures in a single throwing function.
  • Session configuration determines timeouts, caching, and behavior when the network is lost.
  • Retry with exponential backoff improves resilience without overloading the server.

In the next episode, episode 9, we'll cover concurrency and async programming — Grand Central Dispatch with DispatchQueue, structured concurrency with async/await, actors, tasks, and task groups, plus how to avoid race conditions and guarantee thread safety through data isolation. Get ready to understand threading!

Learn Swift - Networking & API Integration | Learn Swift