{"id":20492574,"url":"https://github.com/redmadrobot/apexy-ios","last_synced_at":"2025-04-13T17:03:48.399Z","repository":{"id":44618230,"uuid":"285838377","full_name":"RedMadRobot/apexy-ios","owner":"RedMadRobot","description":"The library for organizing a network layer in your awesome project.","archived":false,"fork":false,"pushed_at":"2024-01-19T16:30:20.000Z","size":1334,"stargazers_count":44,"open_issues_count":4,"forks_count":9,"subscribers_count":9,"default_branch":"master","last_synced_at":"2024-03-21T04:50:38.481Z","etag":null,"topics":["alamofire","http","ios","network","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RedMadRobot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2020-08-07T13:41:06.000Z","updated_at":"2024-01-20T08:49:22.000Z","dependencies_parsed_at":"2023-12-20T16:17:40.069Z","dependency_job_id":null,"html_url":"https://github.com/RedMadRobot/apexy-ios","commit_stats":{"total_commits":88,"total_committers":15,"mean_commits":5.866666666666666,"dds":0.5795454545454546,"last_synced_commit":"c293b10bc3544bb35b1f1c0d91b404aca0f41928"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RedMadRobot%2Fapexy-ios","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RedMadRobot%2Fapexy-ios/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RedMadRobot%2Fapexy-ios/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RedMadRobot%2Fapexy-ios/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RedMadRobot","download_url":"https://codeload.github.com/RedMadRobot/apexy-ios/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224819488,"owners_count":17375275,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alamofire","http","ios","network","swift"],"created_at":"2024-11-15T17:29:40.053Z","updated_at":"2024-11-15T17:29:41.296Z","avatar_url":"https://github.com/RedMadRobot.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"Images/apexy.png\"/\u003e\n\n# Apexy\n\n[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Apexy.svg)](https://cocoapods.org/pods/Apexy)\n[![Platform](https://img.shields.io/cocoapods/p/Apexy.svg?style=flat)](https://cocoapods.org/pods/Apexy)\n[![SPM compatible](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager)\n[![Swift 5.3](https://img.shields.io/badge/swift-5.3-red.svg?style=flat)](https://developer.apple.com/swift)\n[![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/RedMadRobot/api-client-ios/blob/master/LICENSE)\n[![codebeat badge](https://codebeat.co/badges/2cf939f7-b511-43c6-a977-6907478af759)](https://codebeat.co/projects/github-com-redmadrobot-api-client-ios-master)\n\nThe library for organizing a network layer in a project.\n\n- Separate the objects to work with the network in a separate module, target or library, so that they are isolated in their `namespace`.\n- Break down requests into separate structures. Classes are not forbidden, but make them non-mutable. Use `enum` if different requests have the same response.\n\n## Installation\n\n### CocoaPods\n\nTo integrate Apexy into your Xcode project using CocoaPods, specify it in your Podfile.\n\nIf you want to use Apexy with Alamofire:\n\n`pod 'Apexy'`\n\nIf you want to use Apexy without Alamofire:\n\n`pod 'Apexy/URLSession'`\n\nIf you want to use [ApexyLoader](Documentation/loader.md):\n\n`pod 'Apexy/Loader'`\n\n### Swift Package Manager\n\nIf you have Xcode project, open it and select **File → Swift Packages → Add package Dependency** and paste Apexy repository URL:\n\n`https://github.com/RedMadRobot/apexy-ios`\n\nThere are 3 package products: Apexy, ApexyAlamofire, ApexyLoader.\n\nApexy — Uses URLSession under the hood\n\nApexyAlamofire — Uses Alamofire under the hood\n\nApexyLoader — add-on for Apexy to store fetched data in memory and observe loading state. See the documentation for details [ApexyLoader](Documentation/loader.md):\n\nIf you have your own Swift package, add Apexy as a dependency to the dependencies value of your Package.swift.\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/RedMadRobot/apexy-ios.git\")\n]\n```\n\n## Endpoint\n\n`Endpoint` - one of the basic protocols for organizing work with REST API. It is a set of request and response processing.\n\n\u003e Must not be mutable.\n\n1. Creates `URLRequest` for sending the request.\n2. Validates a server response for API errors.\n3. Converts a server response to the right type (`Data`, `String`, `Decodable`).\n\n```swift\npublic struct Book: Codable, Identifiable {\n    public let id: String\n    public let name: String\n}\n\npublic struct BookEndpoint: Endpoint {\n    public typealias Content = Book\n\n    public let id: Book.ID\n\n    public init(id: Book.ID) {\n        self.id = id\n    }\n\n    public func makeRequest() throws -\u003e URLRequest {\n        let url = URL(string: \"books\")!.appendingPathComponent(id)\n        return URLRequest(url: url)\n    }\n\n    public func validate(_ response: URLResponse?, with body: Data) throws {\n        // TODO: check API / HTTP error\n    }\n\n    public func content(from response: URLResponse?, with body: Data) throws -\u003e Content {\n        return try JSONDecoder().decode(Content.self, from: body)\n    }\n}\n\nlet client = Client ...\n\nlet endpoint = BookEndpoint(id: \"1\")\nclient.request(endpoint) { (result: Result\u003cBook, Error\u003e)\n    print(result)\n}\n```\n\n## Client\n\n`Client` - an object with only one method for executing `Endpoint`.\n- It's easy to mock, because it has only one method.\n- It's easy to send several `Endpoint`.\n- Easily wraps into decorators or adapters. For example, you can wrap in `Combine` and you don't have to make wrappers for each request.\n\nThe separation into `Client` and `Endpoint` allows you to separate the asynchronous code in `Client` from the synchronous code in `Endpoint`. Thus, the side effects are isolated in `Client`, and the pure functions in the non-mutable `Endpoint`.\n\n### CombineClient\n\n`CombineClient` - protocol that wrap up network request in `Combine`.\n\n### ConcurrencyClient\n\n`ConcurrencyClient` - protocol that wrap up network request in `Async/Await`.\n\n* By default, new methods implemented as extensions of `Client`'s methods.\n* `ApexyAlamofire` use built in implementation of `Async/Await` in `Alamofire`\n* For `URLSession` new `Async/Await` methods was implemented using `URLSession`'s `AsyncAwait` extended implementation for iOS 14 and below. (look into `URLSession+Concurrency.swift` for more details)\n\n`Client`, `CombineClient` and `ConcurrenyClient` are separated protocols. You can specify method that you are using by using specific protocol.\n\n## Getting Started\n\nSince most requests will receive JSON, it is necessary to make basic protocols at the module level. They will contain common requests logic for a specific API.\n\n`JsonEndpoint` - basic protocol for requests waiting for JSON in the response body.\n\n```swift\npublic protocol JsonEndpoint: Endpoint where Content: Decodable {}\n\nextension JsonEndpoint {\n    public func validate(_ response: URLResponse?, with body: Data) throws {\n        // TODO: check API / HTTP error\n    }\n\n    public func content(from response: URLResponse?, with body: Data) throws -\u003e Content {\n        return try JSONDecoder().decode(Content.self, from: body)\n    }\n}\n```\n\n`VoidEndpoint` basic protocol for requests not waiting for a response body.\n```swift\npublic protocol VoidEndpoint: Endpoint where Content == Void {}\n\nextension VoidEndpoint {\n    public func validate(_ response: URLResponse?, with body: Data) throws {\n        // TODO: check API / HTTP error\n    }\n\n    public func content(from response: URLResponse?, with body: Data) throws {}\n}\n```\n\n`BookListEndpoint` - get a list of books.\n```swift\npublic struct BookListEndpoint: JsonEndpoint, URLRequestBuildable {\n    public typealias Content = [Book]\n\n    public func makeRequest() throws -\u003e URLRequest {\n        return get(URL(string: \"books\")!)\n    }\n}\n```\n\n`BookEndpoint` - get a book by `ID`.\n```swift\npublic struct BookEndpoint: JsonEndpoint, URLRequestBuildable {\n    public typealias Content = Book\n\n    public let id: Book.ID\n\n    public init(id: Book.ID) {\n        self.id = id\n    }\n\n    public func makeRequest() throws -\u003e URLRequest {\n        let url = URL(string: \"books\")!.appendingPathComponent(id)\n        return get(url)\n    }\n}\n```\n\n`UpdateBookEndpoint` - update a book.\n\n```swift\npublic struct UpdateBookEndpoint: JsonEndpoint, URLRequestBuildable {\n    public typealias Content = Book\n\n    public let Book: Book\n\n    public func makeRequest() throws -\u003e URLRequest {\n        let url = URL(string: \"books\")!.appendingPathComponent(Book.id)\n        return put(url, body: .json(try JSONEncoder().encode(\"Book\")))\n    }\n}\n```\n\n\u003e For the convenience of `URLRequest` building you can use functions from `HTTP`.\n\n`DeleteBookEndpoint` - delete a book by `ID`.\n\n```swift\npublic struct DeleteBookEndpoint: VoidEndpoint, URLRequestBuildable {\n    public let id: Book.ID\n\n    public init(id: Book.ID) {\n        self.id = id\n    }\n\n    public func makeRequest() throws -\u003e URLRequest {\n        let url = URL(string: \"books\")!.appendingPathComponent(id)\n        return delete(url)\n    }\n}\n```\n\n### Sending a large amount of data to the server\n\nYou can use `UploadEndpoint` to send files or large amounts of data. In the `makeRequest()` method you need to return `URLRequest` and the data you are uploading, it can be a file `.file(URL)`, a data `.data(Data)` or a stream `.stream(InputStream)`. To execute the request, call the `Client.upload(endpoint: completionHandler:)` method. Use `Progress` object to track the progress of the data upload or cancel the request.\n\n```swift\npublic struct FileUploadEndpoint: UploadEndpoint {\n    \n    public typealias Content = Void\n    \n    private let fileUrl: URL\n    \n    \n    init(fileUrl: URL) {\n        self.fileUrl = fileUrl\n    }\n    \n    public func content(from response: URLResponse?, with body: Data) throws {\n        // ...\n    }\n    \n    public func makeRequest() throws -\u003e (URLRequest, UploadEndpointBody) {\n        var request = URLRequest(url: URL(string: \"upload\")!)\n        request.httpMethod = \"POST\"\n        request.setValue(\"application/octet-stream\", forHTTPHeaderField: \"Content-Type\")\n        return (request, .file(fileUrl))\n    }\n}\n```\n\n## Network Layer Organization\n\nIf your application is called `Household`, the network module will be called `HouseholdAPI`.\n\nSplit the network layer into folders:\n- `Model` a folder with network-level models. That's what we send to the server and what we get in the response.\n- `Endpoint` a folder with requests.\n- `Common` a folder with common helpers e.g. `APIError`.\n\n### The final file and folder structure\n\n- Household\n- HouseholdAPI\n  - Model\n    - Book\n  - Endpoint\n    - `JsonEndpoint`\n    - `VoidEndpoint`\n    - Book\n      - `BookListEndpoint`\n      - `BookEndpoint`\n      - `UpdateBookEndpoint`\n      - `DeleteBookEndpoint`\n  - Common\n    - `APIError`\n- HouseholdAPITests\n  - Endpoint\n    - `Book`\n      - `BookListEndpointTests`\n      - `BookEndpointTests`\n      - `UpdateBookEndpointTests`\n      - `DeleteBookEndpointTests`\n\n## Requirements\n\n- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+\n- Xcode 12+\n- Swift 5.3+\n\n## Additional resources\n\n- [Nested response](Documentation/nested_response.md)\n- [Testing](Documentation/tests.md)\n- [Error handling](Documentation/error_handling.md)\n- [Reactive programming](Documentation/reactive.md)\n- [ApexyLoader](Documentation/loader.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fredmadrobot%2Fapexy-ios","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fredmadrobot%2Fapexy-ios","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fredmadrobot%2Fapexy-ios/lists"}