How to Build a Networking Layer with Swift/iOS — Step by Step

Step-by-step tutorial for building a reusable Swift networking layer with URLSession, async/await, and protocol-oriented design for iOS apps.

Published: March 07, 2026

Category: Tech & Development

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 .getUser(let id): return "/users/(id)"        case .createUser: return "/users"        }    }    var method: HTTPMethod {        switch self {        case .getUser: return .GET        case .createUser: return .POST        }    }    var body: [String: Any]? {        switch self {        case .getUser: return nil        case .createUser(let name, let email):            return ["name": name, "email": email]        }    }}

With this structure in place, making any API call is now a single expressive line:

let user = try await networkService.fetch(UserEndpoint.getUser(id: 42), as: User.self)

Best Practices

Always define your endpoints as enums with associated values — this makes them self-documenting and prevents typos in path strings. Inject your NetworkService via protocol so you can swap in a mock during testing. Handle all HTTP status codes explicitly, returning meaningful errors for 401 Unauthorized, 403 Forbidden, and 5xx server errors. Add retry logic with exponential backoff for transient network failures.

Real-World Use Cases at GSoft Technologies

GSoft Technologies has applied this exact networking architecture pattern in iOS projects ranging from real-time sports apps to enterprise inventory management systems. We extend this base layer with token refresh interceptors for OAuth 2.0 authentication, multipart upload support for media-rich apps, and caching layers using URLCache for offline-first experiences. Our reusable networking foundation cuts development time and makes API integrations predictable across every project.

Conclusion

A well-architected Swift networking layer pays dividends throughout the life of your iOS application. By centralizing HTTP logic, embracing protocols, and building for testability from day one, you'll move faster and debug less. Contact GSoft Technologies to build your next iOS project — our team specializes in production-grade Swift architecture that scales with your ambitions.

Back to Blog | Home | Services | Contact Us