Day 23 – Advanced Networking & Error Handling
T
Tuan Beo

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:

  1. URL
    Example: https://api.example.com/posts

  2. HTTP Method
    Example: GET, POST

  3. Headers

    • Content-Type → tells the server the data format you're sending (application/json)

    • Authorization → contains authentication token

  4. 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:

  1. Network layer (no internet, timeout)

  2. Server (400–500 range HTTP status codes)

  3. Decoding (JSON doesn’t match your Swift struct)

  4. 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/await makes async code much easier to read than callbacks


Part 4 – Practice

  1. Use PATCH to update a post’s title.

  2. Add an Authorization header to simulate token-based APIs.

  3. Modify the APIService to log all requests and responses for debugging.

  4. Handle rate limiting (status code 429) by showing a “Please try again later” message.

Comments