Chapter 2 — Requests, responses & Codable
The three parts of a network call
The request
var request = URLRequest(url: URL(string: "https://chirp-api.example.com/auth/login")!)
request.httpMethod = "POST" // the verb
request.setValue("application/json", forHTTPHeaderField: "Content-Type") // "my body is JSON"
request.setValue("application/json", forHTTPHeaderField: "Accept") // "send me JSON back"
request.httpBody = try JSONEncoder().encode(["email": email, "password": password]) // the body
url— where you're calling.httpMethod— the verb:"GET","POST","PATCH","DELETE"(default isGET).- Headers — metadata.
Content-Typetells the server what your body is;Accepttells it what httpBody— the payload, asData. For JSON, you encode a Swift value withJSONEncoder.
Sending it, with async/await
let (data, response) = try await URLSession.shared.data(for: request)
Interpreting the response
guard let http = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
guard (200..<300).contains(http.statusCode) else {
// Non-2xx: the server rejected us. Try to decode its error body.
throw APIError.httpError(status: http.statusCode, data: data)
}
Decoding JSON with Codable
// Chirp/Networking/DTOs/TokenResponse.swift
struct TokenResponse: Decodable {
let accessToken: String
let refreshToken: String
let expiresIn: Int
}
let decoder = JSONDecoder()
let tokens = try decoder.decode(TokenResponse.self, from: data)
Two decoder settings you'll always consider
- Dates. JSON has no date type — dates are strings, and the format varies by API. Chirp sends
decoder.dateDecodingStrategy = .iso8601 - Key naming. Chirp's JSON uses
camelCase(accessToken), matching Swift, so no setting isdecoder.keyDecodingStrategy = .convertFromSnakeCase
Modeling the Chirp responses
// Chirp/Networking/DTOs/Models.swift
import Foundation
struct PublicUser: Decodable, Identifiable, Hashable {
let id: UUID
let username: String
let createdAt: Date
}
struct Post: Decodable, Identifiable, Hashable {
let id: UUID
let text: String
let author: PublicUser
let createdAt: Date
let likeCount: Int
let commentCount: Int
let likedByMe: Bool
}
// The paginated envelope the feed returns.
struct Page<Item: Decodable>: Decodable {
let items: [Item]
let metadata: Metadata
struct Metadata: Decodable { let page: Int; let per: Int; let total: Int }
}
// The consistent error shape the API returns for every failure.
struct APIErrorResponse: Decodable {
let error: Bool
let reason: String
let code: String?
}
PostnestsPublicUser. The API embeds the author object inside each post, andCodablePage<Item>is generic. The API wraps every paginated list in{ items, metadata }, so one
Your first real call, start to finish
enum APIError: Error {
case invalidResponse
case httpError(status: Int, data: Data)
case decoding(Error)
}
func login(email: String, password: String) async throws -> TokenResponse {
// 1. Build the request.
var request = URLRequest(url: URL(string: "https://chirp-api.example.com/auth/login")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(["email": email, "password": password])
// 2. Send it.
let (data, response) = try await URLSession.shared.data(for: request)
// 3. Check the status.
guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse }
guard (200..<300).contains(http.statusCode) else {
throw APIError.httpError(status: http.statusCode, data: data)
}
// 4. Decode the success body.
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
do { return try decoder.decode(TokenResponse.self, from: data) }
catch { throw APIError.decoding(error) }
}
flowchart LR
Build["1. URLRequest
(url, POST, headers, JSON body)"] --> Send["2. await URLSession.data(for:)"] Send --> Status["3. cast HTTPURLResponse
check 2xx"] Status -->|"2xx"| Decode["4. JSONDecoder → TokenResponse"] Status -->|"non-2xx"| Err["throw httpError"] Send -->|"no network"| Err2["throw (URLError)"]
(url, POST, headers, JSON body)"] --> Send["2. await URLSession.data(for:)"] Send --> Status["3. cast HTTPURLResponse
check 2xx"] Status -->|"2xx"| Decode["4. JSONDecoder → TokenResponse"] Status -->|"non-2xx"| Err["throw httpError"] Send -->|"no network"| Err2["throw (URLError)"]
Why this doesn't scale — and what's next
What we built
- Made a real end-to-end call with
URLRequest+await URLSession.data(for:), and learned the - Understood that
URLSessiononly throws for transport errors — you must **check the HTTP status - Decoded JSON with
Codable, configured.iso8601dates (and notedconvertFromSnakeCase), - Saw why per-endpoint functions don't scale, motivating a reusable client.
Mental model to take away
- A call is build the
URLRequest→await data(for:)→ check status → decode — and the status Codablemaps JSON ↔ Swift types by key name; set date and key strategies to match the- Repeating this per endpoint is the problem: factor out the same parts (status, decode, headers,