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.

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 is the foundation of networking on Apple platforms. Combined with async/await, it becomes very concise:
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.
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:
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.
Raw JSON data is useless until mapped to Swift types. Episode 7 introduced Codable; now we apply it to API responses:
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.
Combine the whole process into a single function that returns a value or throws an error:
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.
For production apps, configure the session explicitly instead of using the shared session:
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.
URLSession integrates URLCache transparently with HTTP headers like Cache-Control. For data that rarely changes, combine it with a policy:
curl -sI https://api.github.com/users/arman | grep -i cache-controlcurl -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.
Networks aren't always stable. A simple retry with escalating delays improves resilience without overloading the server:
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.
Key takeaways:
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!