Skip to content

NetworkLayer is a modern, type-safe Swift framework for elegant network communication. It provides a robust foundation for making HTTP requests with features like authentication handling, retry policies, and request processing.

License

Notifications You must be signed in to change notification settings

space-code/network-layer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

77 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

NetworkLayer: Network communication made easy

network-layer

License Swift Compatibility Platform Compatibility CI

Description

NetworkLayer is a modern, type-safe Swift framework for elegant network communication. Built with Swift's async/await concurrency model and actor-based architecture, it provides a robust foundation for making HTTP requests with features like authentication handling, retry policies, and request processing.

Features

✨ Type-Safe Requests - Protocol-based request modeling with compile-time safety
⚑ Async/Await Native - Built for modern Swift concurrency with actor-based thread safety
πŸ” Authentication Support - Built-in authentication interceptor with credential refresh
πŸ”„ Retry Policies - Powered by Typhoon for robust failure handling
🎯 Flexible Configuration - Customizable session configuration, decoders, and delegates
πŸ“± Cross-Platform - Works on iOS, macOS, tvOS, watchOS, and visionOS
⚑ Lightweight - Minimal footprint with focused dependencies
πŸ§ͺ Well Tested - Comprehensive test coverage

Table of Contents

Requirements

Platform Minimum Version
iOS 13.0+
macOS 10.15+
tvOS 13.0+
watchOS 7.0+
visionOS 1.0+
Xcode 15.3+
Swift 5.10+

Installation

Swift Package Manager

Add the following dependency to your Package.swift:

dependencies: [
    .package(url: "https://github.com/space-code/network-layer.git", from: "1.1.0")
]

Or add it through Xcode:

  1. File > Add Package Dependencies
  2. Enter package URL: https://github.com/space-code/network-layer.git
  3. Select version requirements

Architecture

NetworkLayer consists of two packages:

  • NetworkLayer - Core functionality including request processing, session management, and response handling
  • NetworkLayerInterfaces - Protocol definitions and interfaces for extensibility and testing

This separation allows for better modularity and makes it easy to mock components during testing.

Quick Start

import NetworkLayer
import NetworkLayerInterfaces

// Define your request
struct UserRequest: IRequest {
    let userId: String
    
    var domainName: String { "https://api.example.com" }
    var path: String { "users/\(userId)" }
    var httpMethod: HTTPMethod { .get }
}

// Make the request
let requestProcessor = NetworkLayerAssembly().assemble()

do {
    let response: Response<User> = try await requestProcessor.send(UserRequest(userId: "123"))
    print("βœ… User fetched: \(response.value.name)")
} catch {
    print("❌ Request failed: \(error)")
}

Usage

Basic Requests

Define requests by conforming to the IRequest protocol:

import NetworkLayerInterfaces

struct GetPostsRequest: IRequest {
    var domainName: String { "https://jsonplaceholder.typicode.com" }
    var path: String { "posts" }
    var httpMethod: HTTPMethod { .get }
}

struct CreatePostRequest: IRequest {
    let title: String
    let body: String
    let userId: Int
    
    var domainName: String { "https://jsonplaceholder.typicode.com" }
    var path: String { "posts" }
    var httpMethod: HTTPMethod { .post }
    var body: RequestBody? {
        .dictionary([
            "title": title,
            "body": body,
            "userId": userId
        ])
    }
}

// Usage
let requestProcessor = NetworkLayerAssembly().assemble()

// GET request
let posts: Response<[Post]> = try await requestProcessor.send(GetPostsRequest())

// POST request
let newPost: Response<Post> = try await requestProcessor.send(
    CreatePostRequest(title: "Hello", body: "World", userId: 1)
)

Authentication

NetworkLayer supports authentication through the IAuthenticationInterceptor protocol:

import NetworkLayerInterfaces

class BearerTokenInterceptor: IAuthenticationInterceptor {
    private var token: String?
    
    func adapt(request: inout URLRequest, for session: URLSession) async throws {
        if let token = token {
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
    }
    
    func isRequireRefresh(_ request: URLRequest, response: HTTPURLResponse) -> Bool {
        response.statusCode == 401
    }
    
    func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession) async throws {
        // Implement token refresh logic
        token = try await refreshToken()
    }
}

// Configure with authentication
let configuration = Configuration(
    sessionConfiguration: .default,
    interceptor: BearerTokenInterceptor()
)

let requestProcessor = NetworkLayerAssembly().assemble(configuration: configuration)

// Requests requiring authentication
struct SecureRequest: IRequest {
    var domainName: String { "https://api.example.com" }
    var path: String { "secure/data" }
    var httpMethod: HTTPMethod { .get }
    var requiresAuthentication: Bool { true }
}

Retry Policies

Leverage Typhoon for sophisticated retry strategies:

import Typhoon

// Configure retry policy during assembly
let requestProcessor = NetworkLayerAssembly(
    retryStrategy: .custom(
        .exponentialWithJitter(
            retry: 3,
            jitterFactor: 0.2,
            maxInterval: .seconds(30),
            multiplier: 2.0,
            duration: .seconds(1)
        )
    )
).assemble()

// Per-request retry strategy override
let response: Response<Data> = try await requestProcessor.send(
    request,
    strategy: .constant(retry: 5, duration: .seconds(2))
)

// Custom retry evaluation
let response: Response<User> = try await requestProcessor.send(
    request,
    shouldRetry: { error in
        // Only retry on network errors, not on validation failures
        if let networkError = error as? URLError {
            return networkError.code == .timedOut || networkError.code == .networkConnectionLost
        }
        return false
    }
)

Custom Configuration

Customize the network layer to fit your needs:

import NetworkLayer
import NetworkLayerInterfaces

class CustomDelegate: RequestProcessorDelegate {
    func requestProcessor(
        _ processor: IRequestProcessor,
        willSendRequest request: URLRequest
    ) async throws {
        print("Sending request to: \(request.url?.absoluteString ?? "")")
    }
    
    func requestProcessor(
        _ processor: IRequestProcessor,
        validateResponse response: HTTPURLResponse,
        data: Data,
        task: URLSessionTask
    ) throws {
        guard (200...299).contains(response.statusCode) else {
            throw NetworkLayerError.invalidStatusCode(response.statusCode)
        }
    }
}

let configuration = Configuration(
    sessionConfiguration: .default,
    sessionDelegate: CustomSessionDelegate(),
    sessionDelegateQueue: .main,
    jsonDecoder: JSONDecoder(),
    interceptor: BearerTokenInterceptor()
)

let requestProcessor = NetworkLayerAssembly().assemble(
    configuration: configuration,
    delegate: CustomDelegate()
)

Request Validation

Add custom validation logic for responses:

class ValidationDelegate: RequestProcessorDelegate {
    func requestProcessor(
        _ processor: IRequestProcessor,
        validateResponse response: HTTPURLResponse,
        data: Data,
        task: URLSessionTask
    ) throws {
        // Check status code
        guard (200...299).contains(response.statusCode) else {
            throw APIError.invalidStatusCode(response.statusCode)
        }
        
        // Check content type
        guard let contentType = response.value(forHTTPHeaderField: "Content-Type"),
              contentType.contains("application/json") else {
            throw APIError.invalidContentType
        }
        
        // Check response size
        guard data.count > 0 else {
            throw APIError.emptyResponse
        }
    }
}

Common Use Cases

REST API Client

import NetworkLayer
import NetworkLayerInterfaces

class APIClient {
    private let requestProcessor: IRequestProcessor
    
    init() {
        requestProcessor = NetworkLayerAssembly(
            retryStrategy: .custom(
                .exponentialWithJitter(
                    retry: 3,
                    jitterFactor: 0.2,
                    maxInterval: .seconds(30),
                    multiplier: 2.0,
                    duration: .seconds(1)
                )
            )
        ).assemble()
    }
    
    func fetchUser(id: String) async throws -> User {
        struct UserRequest: IRequest {
            let id: String
            var domainName: String { "https://api.example.com" }
            var path: String { "users/\(id)" }
            var httpMethod: HTTPMethod { .get }
        }
        
        let response: Response<User> = try await requestProcessor.send(
            UserRequest(id: id)
        )
        return response.value
    }
    
    func updateUser(_ user: User) async throws -> User {
        struct UpdateUserRequest: IRequest {
            let user: User
            var domainName: String { "https://api.example.com" }
            var path: String { "users/\(user.id)" }
            var httpMethod: HTTPMethod { .put }
            var body: RequestBody? {
                .encodable(user)
            }
        }
        
        let response: Response<User> = try await requestProcessor.send(
            UpdateUserRequest(user: user)
        )
        return response.value
    }
}

Authenticated API Client

import NetworkLayer
import NetworkLayerInterfaces

class SecureAPIClient {
    private let requestProcessor: IRequestProcessor
    
    init(authToken: String) {
        class AuthInterceptor: IAuthenticationInterceptor {
            var token: String
            
            init(token: String) {
                self.token = token
            }
            
            func adapt(request: inout URLRequest, for session: URLSession) async throws {
                request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
            }
            
            func isRequireRefresh(_ request: URLRequest, response: HTTPURLResponse) -> Bool {
                response.statusCode == 401
            }
            
            func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession) async throws {
                // Refresh token logic
            }
        }
        
        let configuration = Configuration(
            sessionConfiguration: .default,
            interceptor: AuthInterceptor(token: authToken)
        )
        
        requestProcessor = NetworkLayerAssembly().assemble(configuration: configuration)
    }
    
    func fetchPrivateData() async throws -> PrivateData {
        struct PrivateDataRequest: IRequest {
            var domainName: String { "https://api.example.com" }
            var path: String { "private/data" }
            var httpMethod: HTTPMethod { .get }
            var requiresAuthentication: Bool { true }
        }
        
        let response: Response<PrivateData> = try await requestProcessor.send(
            PrivateDataRequest()
        )
        return response.value
    }
}

Communication

Documentation

Comprehensive documentation is available: NetworkLayer Documentation

Contributing

We love contributions! Please feel free to help out with this project. If you see something that could be made better or want a new feature, open up an issue or send a Pull Request.

Development Setup

Bootstrap the development environment:

mise install

Author

Nikita Vasilev

Dependencies

This project uses several open-source packages:

  • Atomic - A Swift property wrapper designed to make values thread-safe
  • Typhoon - A service for retry policies with multiple strategies
  • Mocker - A library for mocking data requests using a custom URLProtocol

License

network-layer is available under the MIT license. See the LICENSE file for more info.


⬆ back to top

Made with ❀️ by space-code

About

NetworkLayer is a modern, type-safe Swift framework for elegant network communication. It provides a robust foundation for making HTTP requests with features like authentication handling, retry policies, and request processing.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •