Skip to content

Instantly share code, notes, and snippets.

@kean
Last active Sep 16, 2022
Embed
What would you like to do?
API Client (Archived)
// The MIT License (MIT)
//
// Copyright (c) 2017 Alexander Grebenyuk (github.com/kean).
import Foundation
import Alamofire
import RxSwift
import RxCocoa
// This post is **archived**. For a modern version that uses Async/Await and Actors, see the new article
// [Web API Client in Swift](/post/new-api-client) (Nov 2021).
protocol ClientProtocol {
func request<Response>(_ endpoint: Endpoint<Response>) -> Single<Response>
}
final class Client: ClientProtocol {
private let manager: Alamofire.SessionManager
private let baseURL = URL(string: "<your_server_base_url>")!
private let queue = DispatchQueue(label: "<your_queue_label>")
init(accessToken: String) {
var defaultHeaders = Alamofire.SessionManager.defaultHTTPHeaders
defaultHeaders["Authorization"] = "Bearer \(accessToken)"
let configuration = URLSessionConfiguration.default
// Add `Auth` header to the default HTTP headers set by `Alamofire`
configuration.httpAdditionalHeaders = defaultHeaders
self.manager = Alamofire.SessionManager(configuration: configuration)
self.manager.retrier = OAuth2Retrier()
}
func request<Response>(_ endpoint: Endpoint<Response>) -> Single<Response> {
return Single<Response>.create { observer in
let request = self.manager.request(
self.url(path: endpoint.path),
method: httpMethod(from: endpoint.method),
parameters: endpoint.parameters
)
request
.validate()
.responseData(queue: self.queue) { response in
let result = response.result.flatMap(endpoint.decode)
switch result {
case let .success(val): observer(.success(val))
case let .failure(err): observer(.error(err))
}
}
return Disposables.create {
request.cancel()
}
}
}
private func url(path: Path) -> URL {
return baseURL.appendingPathComponent(path)
}
}
private func httpMethod(from method: Method) -> Alamofire.HTTPMethod {
switch method {
case .get: return .get
case .post: return .post
case .put: return .put
case .patch: return .patch
case .delete: return .delete
}
}
private class OAuth2Retrier: Alamofire.RequestRetrier {
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
if (error as? AFError)?.responseCode == 401 {
// TODO: implement your Auth2 refresh flow
// See https://github.com/Alamofire/Alamofire#adapting-and-retrying-requests
}
completion(false, 0)
}
}
// The MIT License (MIT)
//
// Copyright (c) 2017 Alexander Grebenyuk (github.com/kean).
import Foundation
// MARK: Defines
typealias Parameters = [String: Any]
typealias Path = String
enum Method {
case get, post, put, patch, delete
}
// MARK: Endpoint
final class Endpoint<Response> {
let method: Method
let path: Path
let parameters: Parameters?
let decode: (Data) throws -> Response
init(method: Method = .get,
path: Path,
parameters: Parameters? = nil,
decode: @escaping (Data) throws -> Response) {
self.method = method
self.path = path
self.parameters = parameters
self.decode = decode
}
}
// MARK: Convenience
extension Endpoint where Response: Swift.Decodable {
convenience init(method: Method = .get,
path: Path,
parameters: Parameters? = nil) {
self.init(method: method, path: path, parameters: parameters) {
try JSONDecoder().decode(Response.self, from: $0)
}
}
}
extension Endpoint where Response == Void {
convenience init(method: Method = .get,
path: Path,
parameters: Parameters? = nil) {
self.init(
method: method,
path: path,
parameters: parameters,
decode: { _ in () }
)
}
}
// The MIT License (MIT)
//
// Copyright (c) 2017 Alexander Grebenyuk (github.com/kean).
import Foundation
// MARK: Defining Endpoints
enum API {}
extension API {
static func getCustomer() -> Endpoint<Customer> {
return Endpoint(path: "customer/profile")
}
static func patchCustomer(name: String) -> Endpoint<Customer> {
return Endpoint(
method: .patch,
path: "customer/profile",
parameters: ["name" : name]
)
}
}
final class Customer: Decodable {
let name: String
}
// MARK: Using Endpoints
func test() {
let client = Client(accessToken: "<access_token>")
_ = client.request(API.getCustomer())
_ = client.request(API.patchCustomer(name: "Alex"))
}
@eli007s
Copy link

eli007s commented Dec 11, 2017

how do you get the results from the client.request(API.getCustomers())

@anilabsinc-ajay
Copy link

anilabsinc-ajay commented Jan 5, 2018

Could you create a sample project, It will much better to understand with a project.

@pzmudzinski
Copy link

pzmudzinski commented Apr 9, 2018

This is perfect, but... how to unit test your requests using mock responses? 👍

@olivierto
Copy link

olivierto commented Apr 12, 2018

what about API error handling? i'm pretty new to decodable and i'd like to decode errors after validation.
Any help would be much appreciated.

@DanielCardonaRojas
Copy link

DanielCardonaRojas commented Nov 29, 2018

I really like this abstracion. I thought I'd share an enhancement that can be really useful:

Implement map on Endpoint (make it a functor) like this:

    func map<N>(_ f: @escaping ((Response) throws -> N)) -> Endpoint<N> {
        let newDecodingFuntion: (Data) throws -> N = { data in
            return try f(self.decode(data))
        }
        return Endpoint<N>(method: self.method, path: self.path, parameters: self.parameters, decode: newDecodingFuntion)
    }

This enables transforming the initial expectation of the Endpoint (its Response generic parameter), in some cases you could reuse an endpoint already created.

@hiteshjoshi
Copy link

hiteshjoshi commented Dec 7, 2018

I am getting errors like

load failed with error Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLStringKey=

@kdawgwilk
Copy link

kdawgwilk commented Mar 18, 2019

How would go about adding support for Encodable Parameters?

@paweljankowski
Copy link

paweljankowski commented Mar 21, 2020

Great example! But how to return Response with arrays of codable items? :)

@hareshgediya
Copy link

hareshgediya commented Mar 24, 2020

Awesome! How would to add support for custom JSON initialiser?

@alvarocofre
Copy link

alvarocofre commented Sep 17, 2020

I can't understand how you can Chain two requests with this code.
Please, could you help me with an answer.
Thank you

@OhhhThatVarun
Copy link

OhhhThatVarun commented May 29, 2021

Great example! But how to return Response with arrays of codable items? :)

Hey! Did you get a way to solve this?

@kean
Copy link
Author

kean commented Nov 21, 2021

The original post and the associated code are archived. For a modern version that uses Async/Await and Actors, see the new article Web API Client in Swift (Nov 2021).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment