Skip to content

Repository files navigation

NetworkLayer: Network communication made easy

network-layer

LicenseSwift CompatibilityPlatform CompatibilityCI

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

PlatformMinimum Version
iOS13.0+
macOS10.15+
tvOS13.0+
watchOS7.0+
visionOS1.0+
Xcode15.3+
Swift5.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.2.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
structUserRequest:IRequest{letuserId:StringvardomainName:String{"https://api.example.com"}varpath:String{"users/\(userId)"}varhttpMethod:HTTPMethod{.get }}
// Make the request
letrequestProcessor=NetworkLayerAssembly().assemble()do{letresponse:Response<User>=tryawait 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
structGetPostsRequest:IRequest{vardomainName:String{"https://jsonplaceholder.typicode.com"}varpath:String{"posts"}varhttpMethod:HTTPMethod{.get }}structCreatePostRequest:IRequest{lettitle:Stringletbody:StringletuserId:IntvardomainName:String{"https://jsonplaceholder.typicode.com"}varpath:String{"posts"}varhttpMethod:HTTPMethod{.post }varbody:RequestBody?{.dictionary(["title": title,"body": body,"userId": userId
])}}
// Usage
letrequestProcessor=NetworkLayerAssembly().assemble()
// GET request
letposts:Response<[Post]>=tryawait requestProcessor.send(GetPostsRequest())
// POST request
letnewPost:Response<Post>=tryawait requestProcessor.send(CreatePostRequest(title:"Hello", body:"World", userId:1))

Authentication

NetworkLayer supports authentication through the IAuthenticationInterceptor protocol:

import NetworkLayerInterfaces
classBearerTokenInterceptor:IAuthenticationInterceptor{privatevartoken:String?func adapt(request:inoutURLRequest, for session:URLSession)asyncthrows{iflet 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)asyncthrows{
// Implement token refresh logic
token =tryawaitrefreshToken()}}
// Configure with authentication
letconfiguration=Configuration(
sessionConfiguration:.default,
interceptor:BearerTokenInterceptor())letrequestProcessor=NetworkLayerAssembly().assemble(configuration: configuration)
// Requests requiring authentication
structSecureRequest:IRequest{vardomainName:String{"https://api.example.com"}varpath:String{"secure/data"}varhttpMethod:HTTPMethod{.get }varrequiresAuthentication:Bool{true}}

Retry Policies

Leverage Typhoon for sophisticated retry strategies:

import Typhoon
// Configure retry policy during assembly
letrequestProcessor=NetworkLayerAssembly(
retryStrategy:.custom(.exponentialWithJitter(
retry:3,
jitterFactor:0.2,
maxInterval:.seconds(30),
multiplier:2.0,
duration:.seconds(1)))).assemble()
// Per-request retry strategy override
letresponse:Response<Data>=tryawait requestProcessor.send(
request,
strategy:.constant(retry:5, duration:.seconds(2)))
// Custom retry evaluation
letresponse:Response<User>=tryawait requestProcessor.send(
request,
shouldRetry:{ error in
// Only retry on network errors, not on validation failures
iflet networkError = error as?URLError{return networkError.code ==.timedOut || networkError.code ==.networkConnectionLost
}returnfalse})

Custom Configuration

Customize the network layer to fit your needs:

import NetworkLayer
import NetworkLayerInterfaces
classCustomDelegate:RequestProcessorDelegate{func requestProcessor(
_ processor:IRequestProcessor,
willSendRequest request:URLRequest)asyncthrows{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{throwNetworkLayerError.invalidStatusCode(response.statusCode)}}}letconfiguration=Configuration(
sessionConfiguration:.default,
sessionDelegate:CustomSessionDelegate(),
sessionDelegateQueue:.main,
jsonDecoder:JSONDecoder(),
interceptor:BearerTokenInterceptor())letrequestProcessor=NetworkLayerAssembly().assemble(
configuration: configuration,
delegate:CustomDelegate())

Request Validation

Add custom validation logic for responses:

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

Common Use Cases

REST API Client

import NetworkLayer
import NetworkLayerInterfaces
classAPIClient{privateletrequestProcessor:IRequestProcessorinit(){
requestProcessor =NetworkLayerAssembly(
retryStrategy:.custom(.exponentialWithJitter(
retry:3,
jitterFactor:0.2,
maxInterval:.seconds(30),
multiplier:2.0,
duration:.seconds(1)))).assemble()}func fetchUser(id:String)asyncthrows->User{structUserRequest:IRequest{letid:StringvardomainName:String{"https://api.example.com"}varpath:String{"users/\(id)"}varhttpMethod:HTTPMethod{.get }}letresponse:Response<User>=tryawait requestProcessor.send(UserRequest(id: id))return response.value
}func updateUser(_ user:User)asyncthrows->User{structUpdateUserRequest:IRequest{letuser:UservardomainName:String{"https://api.example.com"}varpath:String{"users/\(user.id)"}varhttpMethod:HTTPMethod{.put }varbody:RequestBody?{.encodable(user)}}letresponse:Response<User>=tryawait requestProcessor.send(UpdateUserRequest(user: user))return response.value
}}

Authenticated API Client

import NetworkLayer
import NetworkLayerInterfaces
classSecureAPIClient{privateletrequestProcessor:IRequestProcessorinit(authToken:String){classAuthInterceptor:IAuthenticationInterceptor{vartoken:Stringinit(token:String){self.token = token
}func adapt(request:inoutURLRequest, for session:URLSession)asyncthrows{
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)asyncthrows{
// Refresh token logic
}}letconfiguration=Configuration(
sessionConfiguration:.default,
interceptor:AuthInterceptor(token: authToken))
requestProcessor =NetworkLayerAssembly().assemble(configuration: configuration)}func fetchPrivateData()asyncthrows->PrivateData{structPrivateDataRequest:IRequest{vardomainName:String{"https://api.example.com"}varpath:String{"private/data"}varhttpMethod:HTTPMethod{.get }varrequiresAuthentication:Bool{true}}letresponse:Response<PrivateData>=tryawait 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

Code of conduct

Contributing

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages