{"id":16699729,"url":"https://github.com/gonzalezreal/simplenetworking","last_synced_at":"2025-03-21T19:32:46.486Z","repository":{"id":52263505,"uuid":"208627095","full_name":"gonzalezreal/SimpleNetworking","owner":"gonzalezreal","description":"Scalable and composable API Clients using Swift Combine","archived":false,"fork":false,"pushed_at":"2021-05-02T20:12:36.000Z","size":117,"stargazers_count":51,"open_issues_count":0,"forks_count":5,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-10-13T18:08:05.085Z","etag":null,"topics":["combine","http-client","logging","networking","stubbing","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/gonzalezreal.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}},"created_at":"2019-09-15T17:02:42.000Z","updated_at":"2023-05-16T09:39:28.000Z","dependencies_parsed_at":"2022-09-12T02:50:55.922Z","dependency_job_id":null,"html_url":"https://github.com/gonzalezreal/SimpleNetworking","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gonzalezreal%2FSimpleNetworking","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gonzalezreal%2FSimpleNetworking/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gonzalezreal%2FSimpleNetworking/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gonzalezreal%2FSimpleNetworking/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gonzalezreal","download_url":"https://codeload.github.com/gonzalezreal/SimpleNetworking/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221818138,"owners_count":16885695,"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":["combine","http-client","logging","networking","stubbing","swift"],"created_at":"2024-10-12T18:08:03.512Z","updated_at":"2024-10-28T10:43:17.487Z","avatar_url":"https://github.com/gonzalezreal.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SimpleNetworking\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fgonzalezreal%2FSimpleNetworking%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/gonzalezreal/SimpleNetworking)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fgonzalezreal%2FSimpleNetworking%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/gonzalezreal/SimpleNetworking)\n[![Twitter: @gonzalezreal](https://img.shields.io/badge/twitter-@gonzalezreal-blue.svg?style=flat)](https://twitter.com/gonzalezreal)\n\n**SimpleNetworking** is a Swift Package that helps you create scalable API clients, simple and elegantly. It uses [Combine](https://developer.apple.com/documentation/combine) to expose API responses, making it easy to compose and transform them.\n\nIt also includes other goodies, like logging and response stubbing.\n\nLet's explore all the features using [The Movie Database API](https://developers.themoviedb.org/3) as an example.\n\n- [Configuring the API client](#configuring-the-api-client)\n- [Creating API requests](#creating-api-requests)\n- [Handling errors](#handling-errors)\n- [Combining and transforming responses](#combining-and-transforming-responses)\n- [Logging requests and responses](#logging-requests-and-responses)\n- [Stubbing responses for API requests](#stubbing-responses-for-api-requests)\n- [Installation](#installation)\n- [Related projects](#related-projects)\n- [Help \u0026 Feedback](#help--feedback)\n\n## Configuring the API client\nThe `APIClient` is responsible for making requests to an API and handling its responses. To create an API client, you need to provide the base URL and, optionally, any additional parameters or headers that you would like to append to all requests, like an API key or an authorization header.\n\n```Swift\nlet tmdbClient = APIClient(\n    baseURL: URL(string: \"https://api.themoviedb.org/3\")!,\n    configuration: APIClientConfiguration(\n        additionalParameters: [\n            \"api_key\": \"20495f041a8caac8752afc86\",\n            \"language\": \"es\",\n        ]\n    )\n)\n```\n\n## Creating API requests\nThe `APIRequest` type contains all the data required to make an API request, as well as the logic to decode valid and error responses from the request's endpoint.\n\nBefore creating an API request, we need to model its valid and error responses, preferably as types conforming to `Decodable`.\n\nUsually, an API defines different valid response models, depending on the request, but a single error response model for all the requests. In the case of The Movie Database API, error responses take the form of a [`Status`](https://www.themoviedb.org/documentation/api/status-codes) value:\n\n```Swift\nstruct Status: Decodable {\n    var code: Int\n    var message: String\n\n    enum CodingKeys: String, CodingKey {\n        case code = \"status_code\"\n        case message = \"status_message\"\n    }\n}\n```\n\nNow, consider the [`GET /genre/movie/list`](https://developers.themoviedb.org/3/genres/get-movie-list) API request. This request returns the official list of genres for movies. We could implement a `GenreList` type for its response:\n\n```Swift\nstruct Genre: Decodable {\n    var id: Int\n    var name: String\n}\n\nstruct GenreList: Decodable {\n    var genres: [Genre]\n}\n```\n\nWith these response models in place, we are ready to create the API request:\n\n```Swift\nlet movieGenresRequest = APIRequest\u003cGenreList, Status\u003e.get(\"/genre/movie/list\")\n```\n\nBut we can do better, and extend `APIClient` to provide a method to get the movie genres:\n\n```Swift\nextension APIClient {\n    func movieGenres() -\u003e AnyPublisher\u003cGenreList, APIClientError\u003cStatus\u003e\u003e {\n        response(for: .get(\"/genre/movie/list\"))\n    }\n}\n```\n\nThe `response(for:)` method takes an `APIRequest` and returns a publisher that wraps sending the request and decoding its response. We can implement all the API methods by relying on it:\n\n```Swift\nextension APIClient {\n    func createSession(with token: Token) -\u003e AnyPublisher\u003cSession, APIClientError\u003cStatus\u003e\u003e {\n        response(for: .post(\"/authentication/session/new\", body: token))\n    }\n    \n    func deleteSession(_ session: Session) -\u003e AnyPublisher\u003cVoid, APIClientError\u003cStatus\u003e\u003e {\n        response(for: .delete(\"/authentication/session\", body: session))\n    }\n    \n    ...\n    \n    func popularMovies(page: Int) -\u003e AnyPublisher\u003cPage\u003cMovie\u003e, APIClientError\u003cStatus\u003e\u003e {\n        response(for: .get(\"/movie/popular\", parameters: [\"page\": page]))\n    }\n    \n    func topRatedMovies(page: Int) -\u003e AnyPublisher\u003cPage\u003cMovie\u003e, APIClientError\u003cStatus\u003e\u003e {\n        response(for: .get(\"/movie/top_rated\", parameters: [\"page\": page]))\n    }\n    \n    ...\n}\n```\n\n## Handling errors\nYour app must be prepared to handle errors when working with an API client. SimpleNetworking provides [`APIClientError`](Sources/SimpleNetworking/APIClientError.swift), which unifies URL loading errors, JSON decoding errors, and specific API error responses in a single generic type.\n\n```Swift\nlet cancellable = tmdbClient.movieGenres()\n    .catch { error in\n        switch error {\n        case .loadingError(let loadingError):\n            // Handle URL loading errors\n            ...\n        case .decodingError(let decodingError):\n            // Handle JSON decoding errors\n            ...\n        case .apiError(let apiError):\n            // Handle specific API errors\n            ...\n        }\n    }\n    .sink { movieGenres in\n        // handle response\n    }\n```\n\nThe generic [`APIError`](Sources/SimpleNetworking/APIError.swift) type provides access to the HTTP status code and the API error response. \n\n## Combining and transforming responses\nSince our API client wraps responses in a [`Publisher`](https://developer.apple.com/documentation/combine/publisher), it is quite simple to combine responses and transform them for presentation.\n\nConsider, for example, that we have to present a list of popular movies, including their title, genre, and cover. To build that list, we need to issue three different requests.\n* [`GET /configuration`](https://developers.themoviedb.org/3/configuration/get-api-configuration), to get the base URL for images.\n* [`GET /genre/movie/list`](https://developers.themoviedb.org/3/genres/get-movie-list), to get the list of official genres for movies.\n* [`GET /movie/popular`](https://developers.themoviedb.org/3/movies/get-popular-movies), to get the list of the current popular movies.\n\nWe could model an item in that list as follows:\n\n```Swift\nstruct MovieItem {\n    var title: String\n    var posterURL: URL?\n    var genres: String\n    \n    init(movie: Movie, imageBaseURL: URL, movieGenres: GenreList) {\n        self.title = movie.title\n        self.posterURL = imageBaseURL\n            .appendingPathComponent(\"w300\")\n            .appendingPathComponent(movie.posterPath)\n        self.genres = ...\n    }\n}\n```\n\nTo build the list, we can use the `zip` operator with the publishers returned by the API client.\n\n```Swift\nfunc popularItems(page: Int) -\u003e AnyPublisher\u003c[MovieItem], APIClientError\u003cStatus\u003e\u003e {\n    return Publishers.Zip3(\n        tmdbClient.configuration(),\n        tmdbClient.movieGenres(),\n        tmdbClient.popularMovies(page: page)\n    )\n    .map { (config, genres, page) -\u003e [MovieItem] in\n        let url = config.images.secureBaseURL\n        return page.results.map {\n            MovieItem(movie: $0, imageBaseURL: url, movieGenres: genres)\n        }\n    }\n    .eraseToAnyPublisher()\n}\n```\n\n## Logging requests and responses\nEach `APIClient` instance logs requests and responses using a [SwiftLog](https://github.com/apple/swift-log) logger.\n\nTo see requests and responses logs as they happen, you need to specify the `.debug` log-level when constructing the APIClient.\n\n```Swift\nlet tmdbClient = APIClient(\n    baseURL: URL(string: \"https://api.themoviedb.org/3\")!,\n    configuration: APIClientConfiguration(\n        ...\n    ),\n    logLevel: .debug\n)\n```\n\nSimpleNetworking formats the headers and JSON responses, producing structured and readable logs. Here is an example of the output produced by a [`GET /genre/movie/list`](https://developers.themoviedb.org/3/genres/get-movie-list) request:\n\n```\n2019-12-15T17:18:47+0100 debug: [REQUEST] GET https://api.themoviedb.org/3/genre/movie/list?language=en\n├─ Headers\n│ Accept: application/json\n2019-12-15T17:18:47+0100 debug: [RESPONSE] 200 https://api.themoviedb.org/3/genre/movie/list?language=en\n├─ Headers\n│ access-control-expose-headers: ETag, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, Content-Length, Content-Range\n│ Content-Type: application/json;charset=utf-8\n│ x-ratelimit-reset: 1576426582\n│ Server: openresty\n│ Etag: \"df2617d2ab5d0c85ceff5098b8ab70c4\"\n│ Cache-Control: public, max-age=28800\n│ access-control-allow-methods: GET, HEAD, POST, PUT, DELETE, OPTIONS\n│ Access-Control-Allow-Origin: *\n│ Date: Sun, 15 Dec 2019 16:16:14 GMT\n│ x-ratelimit-remaining: 39\n│ Content-Length: 547\n│ x-ratelimit-limit: 40\n├─ Content\n {\n   \"genres\" : [\n     {\n       \"id\" : 28,\n       \"name\" : \"Action\"\n     },\n     {\n       \"id\" : 12,\n       \"name\" : \"Adventure\"\n     },\n ...\n```\n\n## Stubbing responses for API requests\nStubbing responses can be useful when writing UI or integration tests to avoid depending on network reachability.\n\nFor this task, SimpleNetworking provides `HTTPStubProtocol`, a `URLProtocol` subclass that allows stubbing responses for specific API or URL requests.\n\nYou can stub any `Encodable` value as a valid response for an API request:\n\n```Swift\ntry HTTPStubProtocol.stub(\n    User(name: \"gonzalezreal\"),\n    statusCode: 200,\n    for: APIRequest\u003cUser, Error\u003e.get(\n        \"/user\",\n        headers: [.authorization: \"Bearer 3xpo\"],\n        parameters: [\"api_key\": \"a9a5aac8752afc86\"]\n    ),\n    baseURL: URL(string: \"https://example.com/api\")!\n)\n```\n\nOr as an error response for the same API request:\n\n```Swift\ntry HTTPStubProtocol.stub(\n    Error(message: \"The resource you requested could not be found.\"),\n    statusCode: 404,\n    for: APIRequest\u003cUser, Error\u003e.get(\n        \"/user\",\n        headers: [.authorization: \"Bearer 3xpo\"],\n        parameters: [\"api_key\": \"a9a5aac8752afc86\"]\n    ),\n    baseURL: URL(string: \"https://example.com/api\")!\n)\n```\n\nTo use stubbed responses, you need to pass `URLSession.stubbed` as a parameter when creating an `APIClient` instance:\n\n```Swift\nlet apiClient = APIClient(\n    baseURL: URL(string: \"https://example.com/api\")!,\n    configuration: configuration,\n    session: .stubbed\n)\n```\n\n## Installation\n**Using the Swift Package Manager**\n\nAdd SimpleNetworking as a dependency to your `Package.swift` file. For more information, see the [Swift Package Manager documentation](https://github.com/apple/swift-package-manager/tree/master/Documentation).\n\n```\n.package(url: \"https://github.com/gonzalezreal/SimpleNetworking\", from: \"2.0.0\")\n```\n\n## Related projects\n- [NetworkImage](https://github.com/gonzalezreal/NetworkImage), a Swift µpackage that provides image downloading and caching for your apps. It leverages the foundation `URLCache`, providing persistent and in-memory caches.\n\n## Help \u0026 Feedback\n- [Open an issue](https://github.com/gonzalezreal/SimpleNetworking/issues/new) if you need help, if you found a bug, or if you want to discuss a feature request.\n- [Open a PR](https://github.com/gonzalezreal/SimpleNetworking/pull/new/master) if you want to make some change to `SimpleNetworking`.\n- Contact [@gonzalezreal](https://twitter.com/gonzalezreal) on Twitter.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgonzalezreal%2Fsimplenetworking","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgonzalezreal%2Fsimplenetworking","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgonzalezreal%2Fsimplenetworking/lists"}