REST API design with alamofire, promisekit & swift

When starting a new project I find myself spending some time trying to figure out a maintainable & scalable REST API design in order to avoid unnecessary work later on. I've been experimenting with many different approaches and in this post I want to share my *almost* universally applicable approach that just needs a copy & paste into another project. As the title already suggests, we're going to use Alamofire & PromiseKit in order to handle threads much easier and have a better performance with HTTP requests. 

First of all, we're going to create the fundamental class object which will be the base for everything we do work upon


import Alamofire
import PromiseKit

typealias JSON = [String: Any]

// enum used for convenience purposes (optional)
public enum ServerURL: String {
    case base = "https://api.test" // your link to the api server
    // additional endpoints (e.g. CDN server)
} 

public class RestClient {
    var baseURL: String

    var defaultHeader: HTTPHeaders = [
        "Content-Type": "application/json"
    ]

    public init(baseURL: ServerURL = .base) {
        self.baseURL = baseURL.rawValue
    }
}

First, we create an initializer with the ServerURL which is the base url for each given server. Then we have defaultHeader which is an alias in Alamofire and is just a Dictionary with a string key and string value. defaultHeader is what is required for every request you make. In this case, we just use JSON as out content-type. JSON is just a basic dictionary that will contain all the model and parameter data later on. Our next task would be specifying all the necessary endpoints for our API


public enum Resource {
    case register
    case login
    case getUser(String)

    public var resource: (method: HTTPMethod, route: String) {
        switch self {
        case .register:
            return (.post ,"/user/register")
        case .login:
            return (.post, "/user/login")
        case .getUser(let id):
            return (.get, "/user/\(id)")
        }
    }
}

Here, we're specifying three potential cases our API might have. register & login will be a post request and getUser will be a get request, therefore, the string parameter to provide a user-id. Our resource will be accessible through each keyword and it returns a HTTPMethod (also provided by Alamofire) and its corresponding route which we will append to our baseURL in the request function.


public func request(_ resource: Resource,
                    parameters: [String: Any] = [:],                                    
                    headers: HTTPHeaders = [:]) -> Promise < JSON > { 
            return Promise { fulfill, reject in

            let method = resource.resource.method
            let url = "\(baseURL)\(resource.resource.route)"
            var header = defaultHeader

            /* 
            prepare additional stuff like:
                - more header parameters
                - edit/change the endpoint when something specifically is called
                - create a signature for security purposes
                etc.
            */    

            Alamofire.request(u, method: method,
                                 parameters: method == .get ? nil : params,        
                                 encoding: JSONEncoding.default,
                                 headers: headers).responseJSON { (response) in

                                    switch response.result {
                                    case .success(let json):
                                      // If there is not JSON data, cause an error (`reject` function)
                                       guard let json = json as? JSON else { 
                                         return reject(AFError.responseValidationFailed(reason: .dataFileNil))
                                       }
                                      // pass the JSON data into the fulfill function, so we can receive the value
                                      fulfill(json)
                                   case .failure(let error):
                                      // pass the error into the reject function, so we can check what causes the error
                                      reject(error)     
                                  }
                          }
                  }
          }

The request function takes three parameters with two optional ones. Here, we have optional parameters since it's being mostly used for PATCH or POST requests. headers will allow us to pass additional info in cases like an image upload. Otherwise, we will use our defaultHeader as previously specified. The Alamofire request takes five parameters.

- URL

- method (which we have in our Resource

- parameters (we set it nil in case it's a get request for which we don't need any parameters)

- encoding method (we will use JSON as it's the most common used)

- and headers which we have specified

After that, we let Alamofire know that we want our response in JSON format. In that closure, we will check whether that request was a success. If not, we will call the reject function. Later, when we will perform the request, we won't receive the value but the error. If the request was successful, we just pass the result into the fulfill function. It will just allow us to access the JSON data after each request call.

This is now a very simplified design. However, an API might also have an additional endpoint for image uploads. For that, you can create a similar function to request since Alamofire also supports uploading data.

Now, I'm going to design a potential service class that we will use for different endpoints that have different baseURL's.


import PromiseKit
public class UserService: RestClient {

    init() {
        super.init(baseURL: .base)
    }

    public func login(_ parameters: JSON) -> Promise < JSON > {
        return request(.login, parameters: parameters)
    }

    public func register(_ info: JSON) -> Promise < JSON > {
        return request(.register, parameters: parameters)
    }

    public func getUser(_ id: String) -> Promise < JSON > {
        return request(.getUser(id))
    }
}

We can inherit from our RestClient and use the request functions and the initializer much more conveniently. See, how this allows us to implement all of our endpoints in no time? Additionally, we have a very readable and clean code. This way, we can expand to more services, such as an image-service. What we need to do is to create a new class with the baseURL for the image-service and change its parameters in the request function. For an upload we might use the Data value type in the parameters of the function which consists of the image data.

Now, we're ready to use our API.


import UIKit
class LoginViewController: UIViewController {
    private let service = UserService()

    /* 
     create textfields for user input
    */

    override func viewDidLoad() {
        super.viewDidLoad()
        // setup UI
    }

    // function used as the selector for our UIButton
    @objc private func login() {
        let email = emailTextfield.text!
        let password = passwordTextfield.text!

        firstly {
          self.service.login(["email": email, "password": password])
         }.then { json -> Promise < JSON > in 
          // maybe you get a profileId in the JSON data
          return self.service.getUser(json["profileId"])
         }.then { json -> Void in 
          // handle successful request         
         }.catch { error in 
          // handle error
         }
    }
}

As you can see, we're able to call the the functions sequentially without having to worry about concurrency issues. These are the most basic features of PromiseKit. It also allows us to perform an array of asynchronous request and wait until each of them have been performed. However, I will not cover it in this blogpost.

To me, I've found this design to be very convenient to work with 90% of all projects. It simplifies complexity, scales very well and it's easy to read.
 

I hope you liked it and found it useful!