
Day 23 – Advanced Networking & Error Handling
Understand HTTP methods (GET, POST, PUT, DELETE), request headers, sending data in the request body, and building a reusable networking layer with proper error handling.
Part 1 – Theory
1. HTTP Methods and When to Use Them
When working with REST APIs, the most common HTTP methods are:
Method | Purpose | Example |
|---|---|---|
GET | Retrieve data | Get a list of posts |
POST | Create a new resource | Add a new user |
PUT | Replace an existing resource | Update user info completely |
PATCH | Update part of a resource | Update only a user's email |
DELETE | Remove a resource | Delete a comment |
Example:
GET /posts → Get all posts
GET /posts/1 → Get a single post
POST /posts → Create a new post
PUT /posts/1 → Replace post 1
DELETE /posts/1 → Delete post 1
2. Request Components
A network request usually has:
URL
Example:https://api.example.com/postsHTTP Method
Example:GET,POSTHeaders
Content-Type→ tells the server the data format you're sending (application/json)Authorization→ contains authentication token
Body (optional)
Used for sending data in POST/PUT/PATCH requests
Usually JSON in modern APIs
3. Understanding Responses
Every API responds with:
Status Code
200 → OK
201 → Created
400 → Bad Request (your request was wrong)
401 → Unauthorized (need to log in)
404 → Not Found
500 → Server Error
Headers (metadata like cache-control, content-type)
Body (data in JSON, XML, HTML, etc.)
4. Error Handling in Networking
Errors can come from:
Network layer (no internet, timeout)
Server (400–500 range HTTP status codes)
Decoding (JSON doesn’t match your Swift struct)
App logic (invalid data in your model)
We usually create custom error types to handle these cases.
5. Why use a Networking Layer?
Instead of putting URLSession calls directly in views, we make a service class:
Reusable for multiple requests
Centralized error handling
Easy to unit test
Part 2 – Implementation
We’ll extend yesterday’s posts example but add:
Custom errors
POST request
Networking service
Step 1 – Define APIError
enum APIError: Error, LocalizedError {
case invalidURL
case requestFailed(Int)
case decodingFailed
case unknown(Error)
var errorDescription: String? {
switch self {
case .invalidURL:
return "The API URL is invalid."
case .requestFailed(let code):
return "Request failed with status code \(code)."
case .decodingFailed:
return "Failed to decode the response."
case .unknown(let error):
return error.localizedDescription
}
}
}
Step 2 – Create a Networking Service
struct APIService {
func fetch<T: Decodable>(_ type: T.Type, from urlString: String) async throws -> T {
guard let url = URL(string: urlString) else {
throw APIError.invalidURL
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.unknown(URLError(.badServerResponse))
}
guard httpResponse.statusCode == 200 else {
throw APIError.requestFailed(httpResponse.statusCode)
}
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw APIError.decodingFailed
}
} catch {
throw APIError.unknown(error)
}
}
func post<T: Encodable, U: Decodable>(
_ body: T,
to urlString: String,
responseType: U.Type
) async throws -> U {
guard let url = URL(string: urlString) else {
throw APIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(body)
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.unknown(URLError(.badServerResponse))
}
guard (200...299).contains(httpResponse.statusCode) else {
throw APIError.requestFailed(httpResponse.statusCode)
}
do {
return try JSONDecoder().decode(U.self, from: data)
} catch {
throw APIError.decodingFailed
}
} catch {
throw APIError.unknown(error)
}
}
}
Step 3 – Use it in a ViewModel
@MainActor
final class PostsViewModel: ObservableObject {
@Published var posts: [Post] = []
@Published var errorMessage: String?
private let service = APIService()
func loadPosts() async {
do {
posts = try await service.fetch([Post].self,
from: "https://jsonplaceholder.typicode.com/posts")
} catch {
errorMessage = error.localizedDescription
}
}
func createPost() async {
let newPost = Post(userId: 1, id: 0, title: "New Post", body: "Hello world")
do {
let created: Post = try await service.post(
newPost,
to: "https://jsonplaceholder.typicode.com/posts",
responseType: Post.self
)
posts.insert(created, at: 0)
} catch {
errorMessage = error.localizedDescription
}
}
}
Step 4 – SwiftUI View
struct ContentView: View {
@StateObject private var vm = PostsViewModel()
var body: some View {
NavigationStack {
List(vm.posts) { post in
VStack(alignment: .leading) {
Text(post.title).font(.headline)
Text(post.body).font(.subheadline).foregroundStyle(.secondary)
}
}
.navigationTitle("Posts")
.toolbar {
Button("Add Post") {
Task { await vm.createPost() }
}
}
.task { await vm.loadPosts() }
}
}
}
Part 3 – Key Takeaways
Always check status codes — 200 is not the only “OK”, but 4xx/5xx mean failure
Create custom error enums to handle API errors in one place
Use a Networking Service to keep code clean and reusable
async/awaitmakes async code much easier to read than callbacks
Part 4 – Practice
Use
PATCHto update a post’s title.Add an
Authorizationheader to simulate token-based APIs.Modify the
APIServiceto log all requests and responses for debugging.Handle rate limiting (status code 429) by showing a “Please try again later” message.
