{"id":20407745,"url":"https://github.com/diamirio/endpoints","last_synced_at":"2025-12-11T22:51:01.923Z","repository":{"id":42186694,"uuid":"82849612","full_name":"diamirio/Endpoints","owner":"diamirio","description":"Type-Safe Swift Networking","archived":false,"fork":false,"pushed_at":"2025-02-05T08:19:16.000Z","size":1125,"stargazers_count":47,"open_issues_count":1,"forks_count":7,"subscribers_count":6,"default_branch":"develop","last_synced_at":"2025-03-13T05:30:29.264Z","etag":null,"topics":["generics","ios","macos","networking","parsing","swift","swift-5","tvos","watchos"],"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/diamirio.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-02-22T20:32:16.000Z","updated_at":"2025-03-07T16:23:14.000Z","dependencies_parsed_at":"2023-11-07T13:39:44.824Z","dependency_job_id":"85564c3f-ca72-4cb3-9831-2fe45ede0500","html_url":"https://github.com/diamirio/Endpoints","commit_stats":{"total_commits":263,"total_committers":9,"mean_commits":29.22222222222222,"dds":0.4334600760456274,"last_synced_commit":"9a695b7b2fcde36110b1fdb7e636713cbd760dc3"},"previous_names":["diamirio/endpoints","tailoredmedia/endpoints"],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diamirio%2FEndpoints","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diamirio%2FEndpoints/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diamirio%2FEndpoints/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diamirio%2FEndpoints/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/diamirio","download_url":"https://codeload.github.com/diamirio/Endpoints/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244066180,"owners_count":20392406,"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":["generics","ios","macos","networking","parsing","swift","swift-5","tvos","watchos"],"created_at":"2024-11-15T05:26:02.926Z","updated_at":"2025-12-11T22:51:01.916Z","avatar_url":"https://github.com/diamirio.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://github.com/diamirio/AsyncReactor/assets/19715246/56eef378-e63e-4732-8710-040d3440afbb\"\u003e\n  \u003cimg alt=\"DIAMIR Logo\" src=\"https://github.com/diamirio/AsyncReactor/assets/19715246/8424fef3-5aeb-4e15-af36-55f1f3fc37b0\"\u003e\n\u003c/picture\u003e\n\n# Endpoints\n\n[![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager)\n\nEndpoints makes it easy to write a type-safe network abstraction layer for any Web-API.\n\nIt requires Swift 6.2+, makes heavy use of generics and protocols (with protocol extensions). It also encourages a clean separation of concerns and the use of value types (i.e. structs). Built for modern Swift concurrency with async/await and actor support.\n\n**Key Features:**\n- **Type-safe API**: Strongly typed requests and responses\n- **Swift 6.2+**: Full support for Swift's strict concurrency model\n- **Actor-based Session**: Thread-safe networking with `Session` as an actor\n- **Sendable conformance**: All core protocols require `Sendable` conformance for safe concurrent access\n- **Async/await**: Native async/await support throughout the API\n- **Flexible parsing**: Multiple built-in response parsers with support for custom parsers\n- **JSON Codable**: First-class support for `Codable` types\n\n## Requirements\n\n* Swift 6.2+\n* iOS 13+\n* tvOS 12+\n* macOS 10.15+\n* watchOS 6+\n* visionOS 1+\n\n## Installation\n\n**Swift Package Manager:**\n\n```swift\n.package(url: \"https://github.com/diamirio/Endpoints.git\", .upToNextMajor(from: \"4.0.0\"))\n```\n\n## Usage\n\n### Basics\n\nHere's how to load a random image from Giphy.\n\n```swift\n// A client is responsible for encoding and parsing all calls for a given Web-API.\nlet client = DefaultClient(url: URL(string: \"https://api.giphy.com/v1/\")!)\n\n// A call encapsulates the request that is sent to the server and the type that is expected in the response.\nlet call = AnyCall\u003cDataResponseParser\u003e(Request(.get, \"gifs/random\", query: [\"tag\": \"cat\", \"api_key\": \"dc6zaTOxFJmzC\"]))\n\n// A session is an actor that wraps `URLSession` and allows you to start the request for the call and get the parsed response object (or an error).\n// Session is an actor, ensuring thread-safe access to URLSession.\nlet session = Session(with: client)\n\n// Start call - returns the parsed body and HTTPURLResponse\nlet (body, httpResponse) = try await session.dataTask(for: call)\n```\n\n### Response Parsing\n\nA call is supposed to know exactly what response to expect from its request. It delegates the parsing of the response to a `ResponseParser`.\n\nSome built-in types already adopt the `ResponseParser` protocol (using protocol extensions), so you can for example turn any response into a JSON array or dictionary:\n\n```swift\n// Replace `DataResponseParser` with any `ResponseParser` implementation\nlet call = AnyCall\u003cDictionaryParser\u003cString, Any\u003e\u003e(Request(.get, \"gifs/random\", query: [\"tag\": \"cat\", \"api_key\": \"dc6zaTOxFJmzC\"]))\n\n...\n\n// body is now a JSON dictionary 🎉\nlet (body, httpResponse) = try await session.dataTask(for: call)\n````\n\n```swift\nlet call = AnyCall\u003cJSONParser\u003cGiphyGif\u003e\u003e(Request(.get, \"gifs/random\", query: [\"tag\": \"cat\", \"api_key\": \"dc6zaTOxFJmzC\"]))\n\n...\n\n// body is now a `GiphyGif` dictionary 🎉\nlet (body, httpResponse) = try await session.dataTask(for: call)\n```\n\n#### Provided `ResponseParser`s\n\nLook up the documentation in the code for further explanations of the types.\n\n* `DataResponseParser`\n* `DictionaryParser`\n* `JSONParser`\n* `NoContentParser`\n* `StringConvertibleParser`\n* `StringParser`\n\n#### JSON Codable Integration\n\n`Endpoints` has built-in JSON Codable support.\n\n##### Decoding\n\nThe `ResponseParser` responsible for handling decodable types is the `JSONParser`.\n\nThe default `JSONParser` comes pre-configured with:\n- `dateDecodingStrategy = .iso8601`\n- `keyDecodingStrategy = .convertFromSnakeCase`\n\n```swift\n// Decode a type using the default decoder (with iso8601 dates and snake_case conversion)\nstruct GiphyCall: Call {\n    typealias Parser = JSONParser\u003cGiphyGif\u003e\n\n    var request: URLRequestEncodable {\n        Request(.get, \"gifs/random\", query: [\"tag\": \"cat\"])\n    }\n}\n\n// If you need different decoder settings, create a custom parser\n// Note: T must be Sendable for Swift 6.2+ concurrency safety\nstruct CustomJSONParser\u003cT: Decodable \u0026 Sendable\u003e: ResponseParser {\n    typealias OutputType = T\n\n    let jsonDecoder: JSONDecoder\n\n    init() {\n        let decoder = JSONDecoder()\n        decoder.dateDecodingStrategy = .secondsSince1970\n        decoder.keyDecodingStrategy = .useDefaultKeys\n        self.jsonDecoder = decoder\n    }\n\n    func parse(data: Data, encoding: String.Encoding) throws -\u003e T {\n        try jsonDecoder.decode(T.self, from: data)\n    }\n}\n\nstruct GiphyCall: Call {\n    typealias Parser = CustomJSONParser\u003cGiphyGif\u003e\n\n    var request: URLRequestEncodable {\n        Request(.get, \"gifs/random\", query: [\"tag\": \"cat\"])\n    }\n}\n```\n\n##### Encoding\n\nEvery encodable is able to provide a `JSONEncoder()` to encode itself via the `toJSON()` method.\n\n### Dedicated Calls\n\n`AnyCall` is the default implementation of the `Call` protocol, which you can use as-is. But if you want to make your networking layer really type-safe you'll want to create a dedicated `Call` type for each operation of your Web-API.\n\n**Note:** All `Call` types must conform to `Sendable` for Swift 6.2+ concurrency safety. Use value types (structs) with sendable properties:\n\n```swift\nstruct GetRandomImage: Call {\n    typealias Parser = DictionaryParser\u003cString, Any\u003e\n\n    var tag: String\n\n    var request: URLRequestEncodable {\n        return Request(.get, \"gifs/random\", query: [ \"tag\": tag, \"api_key\": \"dc6zaTOxFJmzC\" ])\n    }\n}\n\n// `GetRandomImage` is much safer and easier to use than `AnyCall`\nlet call = GetRandomImage(tag: \"cat\")\n```\n\n### Dedicated Clients\n\nA client is responsible for handling things that are common for all operations of a given Web-API. Typically this includes appending API tokens or authentication tokens to a request or validating responses and handling errors.\n\n`DefaultClient` is the default implementation of the `Client` protocol and can be used as-is or as a starting point for your own dedicated client.\n\nYou'll usually need to create your own dedicated client that implements the `Client` protocol and delegates the encoding of requests and parsing of responses to a `DefaultClient` instance, as done here.\n\n**Note:** All `Client` types must conform to `Sendable`. Use structs with sendable properties to ensure thread-safety:\n\n```swift\nstruct GiphyClient: Client {\n    private let client: Client\n    let apiKey = \"dc6zaTOxFJmzC\"\n\n    init() {\n        let url = URL(string: \"https://api.giphy.com/v1/\")!\n        self.client = DefaultClient(url: url)\n    }\n\n    func encode(call: some Call) async throws -\u003e URLRequest {\n        var request = try await client.encode(call: call)\n\n        // Append the API key to every request's URL\n        if let url = request.url,\n           var components = URLComponents(url: url, resolvingAgainstBaseURL: true) {\n            var queryItems = components.queryItems ?? []\n            queryItems.append(URLQueryItem(name: \"api_key\", value: apiKey))\n            components.queryItems = queryItems\n            request.url = components.url\n        }\n\n        return request\n    }\n\n    func parse\u003cC\u003e(response: HTTPURLResponse?, data: Data?, for call: C) async throws -\u003e C.Parser.OutputType\n        where C: Call {\n        do {\n            // Use `DefaultClient` to parse the response\n            // If this fails, try to read error details from response body\n            return try await client.parse(response: response, data: data, for: call)\n        } catch {\n            // See if the backend sent detailed error information\n            guard\n                let response,\n                let data,\n                let errorDict = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any],\n                let meta = errorDict?[\"meta\"] as? [String: Any],\n                let errorCode = meta[\"error_code\"] as? String\n            else {\n                // no error info from backend -\u003e rethrow default error\n                throw error\n            }\n\n            // Propagate error that contains errorCode as reason from backend\n            throw StatusCodeError.unacceptable(code: response.statusCode, reason: errorCode)\n        }\n    }\n\n    func validate(response: HTTPURLResponse?, data: Data?) async throws {\n        // Delegate to the default client's validation\n        try await client.validate(response: response, data: data)\n    }\n}\n```\n\n### Dedicated Response Types\n\nYou usually want your networking layer to provide a dedicated response type for every supported call. In our example this could look like this:\n\n**Note:** Response types must conform to `Sendable` for Swift 6.2+ concurrency safety:\n\n```swift\nstruct RandomImage: Decodable, Sendable {\n    struct Data: Decodable, Sendable {\n        let url: URL\n\n        private enum CodingKeys: String, CodingKey {\n            case url = \"image_url\"\n        }\n    }\n\n    let data: Data\n}\n\nstruct GetRandomImage: Call {\n    typealias Parser = JSONParser\u003cRandomImage\u003e\n\n    var tag: String\n\n    var request: URLRequestEncodable {\n        Request(.get, \"gifs/random\", query: [\"tag\": tag])\n    }\n}\n```\n\n### Type-Safety\n\nWith all the parts in place, users of your networking layer can now perform type-safe requests and get a type-safe response with a few lines of code:\n\n```swift\nlet client = GiphyClient()\nlet call = GetRandomImage(tag: \"cat\")\nlet session = Session(with: client)\n\nlet (body, response) = try await session.dataTask(for: call)\nprint(\"image url: \\(body.data.url)\")\n```\n\n## Example\n\nExample implementation can be found [here](https://github.com/diamirio/Endpoints-Example).\n\n## Migration Guides\n\nIf you're upgrading from a previous version, please refer to the migration guides:\n\n- [Migrating from 3.x to 4.x](Migration/V4_0_0.md) - Swift 6.2+ strict concurrency, `AnyClient` → `DefaultClient`, and more\n- [Migrating from 2.x to 3.x](Migration/V3_0_0.md) - Native async/await APIs\n- [Migrating from 1.x to 2.x](Migration/V2_0_0.md)\n\n## Advanced Features\n\n### Debug Logging\n\nEnable debug logging to see detailed request and response information:\n\n```swift\nlet session = Session(with: client, debug: true)\n```\n\nThis will log:\n- cURL representation of the request\n- Response status and headers\n- Response body data\n\n### Request Body Encoding\n\nEndpoints supports multiple body encoding strategies:\n\n```swift\n// JSON encoded body\nlet jsonBody = try JSONEncodedBody(encodable: myModel)\nlet request = Request(.post, \"users\", body: jsonBody)\n\n// Form-urlencoded body\nlet formBody = FormEncodedBody(parameters: [\"username\": \"john\", \"password\": \"secret\"])\nlet request = Request(.post, \"login\", body: formBody)\n\n// Multipart form data (for file uploads)\nlet multipartBody = MultipartBody(parts: [\n    MultipartBody.Part(name: \"avatar\", data: imageData, filename: \"profile.jpg\", mimeType: \"image/jpeg\"),\n    MultipartBody.Part(name: \"name\", data: \"John Doe\".data(using: .utf8)!)\n])\nlet request = Request(.post, \"upload\", body: multipartBody)\n```\n\n### Custom Validation\n\nBoth `Call` and `Client` can implement custom validation logic:\n\n```swift\nstruct MyCall: Call {\n    typealias Parser = JSONParser\u003cMyResponse\u003e\n\n    var request: URLRequestEncodable {\n        Request(.get, \"data\")\n    }\n\n    // Custom validation for this specific call\n    func validate(response: HTTPURLResponse?, data: Data?) async throws {\n        guard let response = response else { return }\n\n        // Require a specific header for this call\n        guard response.value(forHTTPHeaderField: \"X-Custom-Header\") != nil else {\n            throw MyError.missingHeader\n        }\n    }\n}\n\nstruct MyClient: Client {\n    private let client: Client\n\n    init() {\n        self.client = DefaultClient(url: URL(string: \"https://api.example.com\")!)\n    }\n\n    // ... encode and parse implementations ...\n\n    // Custom validation for all calls using this client\n    func validate(response: HTTPURLResponse?, data: Data?) async throws {\n        // First, do the default validation\n        try await client.validate(response: response, data: data)\n\n        // Then add custom validation\n        guard let response = response else { return }\n\n        // Example: Check for maintenance mode\n        if response.statusCode == 503 {\n            throw MaintenanceError()\n        }\n    }\n}\n```\n\n### Error Handling\n\nEndpoints wraps all errors in `EndpointsError`, which includes the `HTTPURLResponse` if available:\n\n```swift\ndo {\n    let (body, response) = try await session.dataTask(for: call)\n    // Handle success\n} catch let error as EndpointsError {\n    // Access the underlying error\n    print(\"Error: \\(error.error)\")\n\n    // Access the HTTP response if available\n    if let response = error.response {\n        print(\"Status code: \\(response.statusCode)\")\n    }\n} catch {\n    // Handle other errors\n    print(\"Unexpected error: \\(error)\")\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiamirio%2Fendpoints","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdiamirio%2Fendpoints","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiamirio%2Fendpoints/lists"}