Introduction Almost every modern iOS app talks to an API. A poorly structured networking layer leads to duplicated code, hard-to-test logic, and brittle apps. In this tutorial, we'll build a clean, reusable Swift networking layer step by step — the same way experienced iOS developers do it in production. What is a Networking Layer in iOS? A networking layer is the dedicated abstraction in your iOS app responsible for all HTTP communication. Rather than scattering URLSession calls throughout your codebase, a well-designed networking layer centralizes all API requests, handles encoding and decoding, manages errors, and makes your network code fully testable. The goal is a clean boundary: your ViewModels and business logic should ask for data, not care about how that data is fetched. This separation makes your app far easier to maintain and extend over time. Key Components of a Swift Networking Layer HTTPMethod enum: Represents GET, POST, PUT, DELETE and other HTTP verbs type-safely. Endpoint protocol: Defines every API endpoint as a structured type with path, method, headers, and body. NetworkService: The core service that executes requests using URLSession and decodes responses. NetworkError: A custom error enum covering all failure scenarios (invalid URL, bad response, decoding failure, etc.). Mock Network Service: A test double that replaces the real service in unit tests. Step-by-Step: Building the Networking Layer Let's build each component from the ground up: import Foundation// MARK: - HTTP Methodenum HTTPMethod: String { case GET, POST, PUT, PATCH, DELETE}// MARK: - Network Errorenum NetworkError: LocalizedError { case invalidURL case badResponse(statusCode: Int) case decodingFailed(Error) case unknown(Error) var errorDescription: String? { switch self { case .invalidURL: return "The URL is invalid." case .badResponse(let code): return "Server returned status code (code)." case .decodingFailed(let err): return "Decoding failed: (err.localizedDescription)" case .unknown(let err): return err.localizedDescription } }}// MARK: - Endpoint Protocolprotocol Endpoint { var scheme: String { get } var host: String { get } var path: String { get } var method: HTTPMethod { get } var headers: [String: String] { get } var body: [String: Any]? { get }}extension Endpoint { var scheme: String { "https" } var host: String { "api.gsofttechnologies.com" } var headers: [String: String] { ["Content-Type": "application/json", "Accept": "application/json"] } var body: [String: Any]? { nil } func buildURLRequest() throws -> URLRequest { var components = URLComponents() components.scheme = scheme components.host = host components.path = path guard let url = components.url else { throw NetworkError.invalidURL } var request = URLRequest(url: url) request.httpMethod = method.rawValue headers.forEach { request.setValue($1, forHTTPHeaderField: $0) } if let body = body { request.httpBody = try JSONSerialization.data(withJSONObject: body) } return request }}// MARK: - Network Serviceprotocol NetworkServiceProtocol { func fetch<T: Decodable>(_ endpoint: some Endpoint, as type: T.Type) async throws -> T}final class NetworkService: NetworkServiceProtocol { private let session: URLSession init(session: URLSession = .shared) { self.session = session } func fetch<T: Decodable>(_ endpoint: some Endpoint, as type: T.Type) async throws -> T { let request = try endpoint.buildURLRequest() let (data, response) = try await session.data(for: request) guard let http = response as? HTTPURLResponse else { throw NetworkError.unknown(URLError(.badServerResponse)) } guard (200...299).contains(http.statusCode) else { throw NetworkError.badResponse(statusCode: http.statusCode) } do { return try JSONDecoder().decode(T.self, from: data) } catch { throw NetworkError.decodingFailed(error) } }}// MARK: - Example Endpointenum UserEndpoint: Endpoint { case getUser(id: Int) case createUser(name: String, email: String) var path: String { switch self { case .getUs