Scaling Moya in production

Apr 11, 2018 17:01 · 4 minute read #swift #moya #iOS

Moya

Moya is a network abstraction layer that leverages Swift’s APIs like enums to make working with the network layer better than ever. However, we often hear that Moya is not scalable in the real world and you’ll end up with this one big enum that’s difficult to maintain. This is the case only if don’t use Moya to its full potential - and in this blog post, I’ll show you some built-in tools to scale Moya in production.

Basic setup

A basic Moya setup is an implementation of TargetType that covers your whole service:

enum GitHub {
    case showUser(id: Int)
    case createUser(firstName: String, lastName: String)
    case updateUser(id: Int, firstName: String, lastName: String)
    case createIssue(repoId: Int, body: String)
    case showIssues(repoId: Int)
}

As you can see we have all endpoints in this one enum. And as long as you have only a few possible endpoints, you’re going to be fine. The problem arrives when you have 200 endpoints and the enum just grows and grows and grows…

Usually, due to many reasons, we just add another case to our TargetType and pray that it will be the last one. But we can do better than this.

Splitting the Massive TargetType

We could try to split the TargetType into multiple smaller ones. For instance, we could split our GitHub target into Users and Issues:

enum GitHubUsers {
    case showUser(id: Int)
    case createUser(firstName: String, lastName: String)
    case updateUser(id: Int, firstName: String, lastName: String)
}

enum GitHubIssues {
    case createIssue(repoId: Int, body: String)
    case showIssues(repoId: Int)
}

But now we have a problem because we need multiple providers for multiple targets:

let usersProvider = MoyaProvider<GitHubUsers>()
let issuesProvider = MoyaProvider<GitHubIssues>()  

With only these two simple providers, that do not contain any additional configuration, it’s manageable. But in a bigger app, you will not only have a lot more targets but maybe also a few plugins like a plugin for authentication or logging. And then our problems begin. We have to pass multiple providers across multiple abstraction layers and we have to duplicate configurations across providers. Fortunately, there is a rescue in the form of MultiTarget.

MultiTarget

By splitting the targets we already did half of the job. The second half is introducing a MultiTarget into your app.

MultiTarget is just a target that connects all other targets. The benefit of that is that you can use only one provider and because of that you’ll have all the configurations specified once.

How do we use it? Pretty straight-forward:

let provider = MoyaProvider<MultiTarget>()
...
provider.request(MultiTarget(GitHubUsers.showUser(id: 1)))

It’s alright, but you still have that additional MultiTarget() overhead initializer in every request. But we can fix it as well.

Quite often when using Moya in bigger apps you have some sort of an abstraction layer (e.g. to handle errors or offline mode). Let’s assume we have this simple abstraction:

struct Network {
    let provider = MoyaProvider<MultiTarget>()
    
    init() {...}
    func request(_ target: MultiTarget, response: (Response) -> Void) {...}
}

We can update our request() function a bit to accept a TargetType instead of a MultiTraget:

func request<T: TargetType>(_ target: T, response: (Response) -> Void) {
    provider.request(MultiTarget(target))...
} 

This way we will have a cleaner call site as well:

network.request(GitHubUsers.showUser(id: 1))

Struct targets

Sometimes there is also a problem with few endpoints that have so complicated logic that even having 2 endpoints in a target makes it too complicated. And that’s when you can switch from an enum to a struct.

It is a little-known fact that Moya also supports structs as targets. And in combination with a MultiTarget, you can make it work pretty smoothly.

Let’s say we’ve got an endpoint that needs to upload a video and before that upload happens, it needs to generate a thumbnail and resize the video to 720p as well. And you have multiple other operations that also happen there. You could, of course, create an additional object that covers the logic before an upload, but it is also doable by extracting this one endpoint to the struct:

struct VideoUploadTarget: TargetType {
    var baseURL: URL
    var path: String
    var method: Method
    var sampleData: Data
    var task: Task
    var headers: [String : String]?
    
    init(videoURL: URL) {
        // some calculations can happen here or lazily, 
        // depends on the use-case
    }
}

And now you can use it with your MultiTarget abstraction to provide an ultimate, scalable experience in your application:

network.request(VideoUploadTarget(videoURL: videoURL))

Conclusion

From my experience, Moya is so flexible that it can handle not only small APIs but also a bigger, commercial product. Using MultiTarget or leveraging structs helps to split one big TargetType into small, manageable objects.

Of course, there are more challenges like how to implement authentication or pagination - but these are focused rather on what is the best/proper way than on if it’s possible or not.

I will try to answer more questions about Moya in a similar format in the future. So, if you have questions about scaling or Moya in general, let me know.

or

Comments