Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
195: Building a Type-Safe API Client Layer
A few years ago, I was reviewing a PR for a teammate who had spent nearly an entire afternoon debugging a 404 error. The API was fine, the server was up, and the authentication was working. It turned out he had a tiny typo in a hardcoded string: "/api/v1/userrs/profile" instead of "/api/v1/users/profile". Now, we've all been there. The problem isn't the typo itself; it's that we're letting the compiler treat our API structure as a collection of "magic strings." When you rely on strings to define your network layer, you're essentially telling Swift, "Trust me, I know how to spell," which is a dangerous bet to make in a codebase with thousands of lines.
Defining the Endpoint Blueprint
To stop these runtime surprises, we need to move the source of truth from strings to types. I prefer using a protocol to define what an "Endpoint" actually is. Instead of passing a URL directly to a session, we create a contract that forces every request to define its own requirements—like the path, the HTTP method, and any necessary query items.
protocol APIEndpoint {
var path: String { get }
var method: HTTPMethod { get }
var queryItems: [URLQueryItem]? { get }
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
By doing this, we've created a blueprint. Now, any part of the app that needs to make a network call doesn't need to know how to build a URL; it just needs to provide an object that conforms to APIEndpoint.
Enforcing Request Integrity with Enums
The real magic happens when you combine that protocol with an enum. Enums are perfect for API clients because they allow us to use associated values for dynamic paths. If you're fetching a specific product by ID, you shouldn't be concatenating strings in your View Model. Instead, you bake that ID right into the enum case.
enum ProductEndpoint: APIEndpoint {
case getAllProducts
case getProductDetails(id: Int)
case updateStock(id: Int, quantity: Int)
var path: String {
switch self {
case .getAllProducts:
return "/products"
case .getProductDetails(let id):
return "/products/\(id)"
case .updateStock(let id, _):
return "/products/\(id)/stock"
}
}
var method: HTTPMethod {
switch self {
case .getAllProducts, .getProductDetails: return .get
case .updateStock: return .put
}
}
var queryItems: [URLQueryItem]? {
return nil // Add specific filters here if needed
}
}
I love this approach because if I ever need to change the path for "Product Details," I change it in exactly one place. The rest of the app just calls .getProductDetails(id: 123) and doesn't care about the underlying string.
Connecting the Pieces with a Generic Client
Now we need a client that can take any APIEndpoint and return a decoded model. The trick here is using generics. We don't want a getProductClient and a getUserClient; we want one client that understands how to handle any type that conforms to Decodable.
class APIClient {
private let baseURL = URL(string: "https://api.mystore.com/v1")!
private let session = URLSession.shared
func request<T: Decodable>(_ endpoint: APIEndpoint) async throws -> T {
var components = URLComponents(url: baseURL.appendingPathComponent(endpoint.path), resolvingAgainstBaseURL: false)
components?.queryItems = endpoint.queryItems
guard let url = components?.url else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = endpoint.method.rawValue
let (data, response) = try await session.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(T.self, from: data)
}
}
Notice how the request method is completely agnostic about what it's fetching. Whether it's a Product, a User, or an Order, the logic remains the same. You've successfully pushed all the "danger" (the strings and the URL construction) into a controlled, type-safe layer. Now, if you try to pass a string where an ID should be, the compiler will yell at you before you ever hit "Run."
📋 Practical Task
Exercise: Refactoring the BookStore API for Type Safety
You have been handed a legacy BookClient class that uses hardcoded strings for its requests, making it prone to typos and difficult to maintain. Your task is to refactor this into a type-safe system.
Requirements:
- Create an
APIEndpointprotocol withpath,method, andqueryItems. - Implement a
BookEndpointenum that conforms toAPIEndpoint. It must handle three cases:listBooks(GET /books)bookDetails(isbn: String)(GET /books/{isbn})addBook(title: String, author: String)(POST /books)
- Create a generic
APIClientclass with arequest<T: Decodable>(_ endpoint: APIEndpoint)method that constructs theURLRequestand decodes the response. - Write a small snippet of code demonstrating how to call
bookDetails(isbn: "978-3-16-148410-0")using your new client.
There are no comments for now.