Overview
In this article, I will show how to deal with API requests in Redux architecture using ReSwift library. I assume that you already know Redux basics. If not I recommend you to check out Getting Started with ReSwift and a great free ebook: The Complete Redux Book.
Problem
Redux by itself doesn’t define where asynchronous operations should be handled. According to the concept, side effects should not be generated by Actions, State or Reducers. Therefore the last place where we could do that is Middleware which is actually designed for additional processing of Actions and generating side effects if needed.
Idea
I will show an approach in which we will implement two things:
- Action representing a generic API call,
- Middleware designed for these Actions, able to trigger network request and dispatch extra Actions once it’s finished.
Implementation
1. HttpRequest Protocol
Let’s first implement a protocol which will be later used to define endpoints conveniently. Each API Action should contain at least free essential information:
- resource – relative endpoint url,
- method – GET/POST etc.,
- optional JSON payload.
Also it should be able to define extra Actions in case of success or failure. Our RestClient will dispatch those Actions once the request is finished.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import Foundation import ReSwift enum HttpMethod: String { case get = "GET" case post = "POST" } protocol HttpRequest { var resource: String { get } var method: HttpMethod { get } var json: Data? { get } func onSuccess(response: Data?) -> [Action] func onFailure(response: Data?) -> [Action] } |
Of course you can add more properties like for example expectedStatusCode if needed, but to simplify implementation I added only essential fields.
2. ApiRequest implementation
Preferably we would like to have generic versions of methods onSuccess and onFailure to avoid boilerplate code in each endpoint definition (like parsing and unwrapping). That’s why base implementation of HttpRequest might be useful.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
class ApiRequest: Action, HttpRequest { let resource: String let method: HttpMethod let json: Data? init(resource: String, method: HttpMethod, json: Data? = nil) { self.resource = resource self.method = method self.json = json } // These methods will be overridden in subclasses func onSuccess(response: T) -> [Action] { return [] } func onFailure(response: ErrorResponse) -> [Action] { return [] } // MARK: HttpRequest implementation // to parse JSON and pass object to generic method func onSuccess(response: Data?) -> [Action] { if let response = response?.toObject(T.self) { return self.onSuccess(response: response) } return [] } func onFailure(response: Data?) -> [Action] { if let error = response?.toObject(ErrorResponse.self) { return self.onFailure(response: error) } return [] } } struct ErrorResponse: Codable { let message: String let code: Int } extension Data { func toObject(_ type: T.Type) -> T? { return try? JSONDecoder().decode(type, from: self) } } |
Having this class we can now easily start defining our API endpoints.
3. Middleware to handle API requests
The last thing we need is to implement a Middleware which will be able to process our API Actions and trigger specific HTTP requests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
static func getApiMiddleware() -> Middleware { return { dispatch, getState in return { next in return { action in next(action) guard let request = action as? HttpRequest else { return } process(request: request, serverUrl: "https://localhost", dispatcher: dispatch) } } } } private static func process(request: HttpRequest, serverUrl: String, dispatcher: @escaping DispatchFunction) { guard let url = URL(string: "\(serverUrl)/\(request.resource)") else { return } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = request.method.rawValue urlRequest.httpBody = request.json // set additional headers if needed let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0 DispatchQueue.main.async { if 200..<300 ~= statusCode && error == nil { request.onSuccess(response: data).forEach(dispatch) } else { // in a real implementation you should also probably pass // statusCode and error to onFailure handler request.onFailure(response: data).forEach(dispatch) } } } task.resume() } |
Usage
Now we have all required elements, so we can easily define endpoints by subclassing ApiRequest.
Sample API Action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class SignIn: ApiRequest { init(credentials: Credentials) { super.init(resource: "api/login", method: .post, json: credentials.toJson()) } override func onSuccess(response: Session) -> [Action] { return [SetSession(session: response)] } override func onFailure(response: ErrorResponse) -> [Action] { return [DisplayError(error: response)] } } |
Dispatching
1 2 3 4 5 |
let store = Store(reducer: Reducers.appReducer, state: AppState(), middleware: [getApiMiddleware()]) let credentials = Credentials(username: "test", password: "test") let action = SignIn(credentials: credentials) store.dispatch(action) |
Summary
Implementation in this article is simplified to show an idea, not a production-ready solution.
Using this approach you gain a clean architecture in which:
- Side effects like network requests are triggered by Middleware keeping State, Actions and Reducers simple.
- Action for each API endpoint can be easily defined by subclassing ApiRequest.
- API Actions allow to define additional Actions for success and failure.
- API Actions are strongly typed and doesn’t require additional parsing or unwrapping.
- Endpoint parameters (like credentials) can be passed through Action initializer.
Check out ReSwiftDemo project to see this code in action:
https://github.com/wojciech-kulik/ReSwiftDemo