Moya
Network abstraction library for Swift. Provides type-safe API definition and network layer abstraction as wrapper around Alamofire. Achieves testable and maintainable network layer through protocol-oriented design. RxSwift and Combine integration support.
GitHub Overview
Moya/Moya
Network abstraction layer written in Swift.
Topics
Star History
Library
Moya
Overview
Moya is "a type-safe and abstracted networking library for Swift" developed as one of the most popular HTTP client libraries in iOS app development. With the concept of "Network abstraction layer for Swift," it provides complex network processing through simple and intuitive APIs. Through type-safe API definitions using the TargetType protocol, extensibility via plugin systems, and complete integration with RxSwift and Combine, it has established itself as the ideal networking solution for Swift developers.
Details
Moya 2025 edition is a mature library that serves as the definitive Swift networking solution with over 10 years of development experience. The TargetType protocol-based design enables defining API endpoints in a type-safe and reusable manner. Plugin architecture efficiently implements cross-cutting concerns such as authentication, logging, caching, and test mocking. Native integration with RxSwift, ReactiveSwift, and Combine supports advanced asynchronous processing using reactive programming patterns. Test-friendly design provides excellent development experience for both unit testing and UI testing.
Key Features
- Type-Safe API Definition: Compile-time type checking through TargetType protocol
- Plugin Architecture: Efficient implementation of authentication, logging, and caching
- Reactive Extensions: Complete integration with RxSwift, Combine, and ReactiveSwift
- Test Support: Rich tools for mocking and test target-specific functionality
- Alamofire-Based: Reliable implementation based on industry-standard library
- Comprehensive Documentation: Rich official documentation and sample code
Pros and Cons
Pros
- Type safety through TargetType protocol significantly reduces runtime errors
- Unified implementation and management of cross-cutting features via plugin system
- Integration with RxSwift/Combine enables declarative and readable asynchronous code
- Rich test support features enable high-quality unit testing and UI testing
- Overwhelming adoption in Swift community with abundant learning resources
- Continuous updates supporting latest Swift features and iOS versions
Cons
- Relatively high learning curve that may feel complex for beginners
- Risk of over-engineering in small projects due to feature richness
- Dependencies can become complex due to plugins and reactive bindings
- Indirect dependency on third-party libraries through Alamofire dependency
- TargetType definition management can become cumbersome for large APIs
- Limited support for platforms other than iOS (macOS, watchOS, etc.)
Reference Pages
Code Examples
Installation and Basic Setup
// Package.swift dependency addition
dependencies: [
.package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "15.0.0"))
],
targets: [
.target(
name: "YourApp",
dependencies: [
"Moya",
// For RxSwift usage
.product(name: "RxMoya", package: "Moya"),
// For Combine usage
.product(name: "CombineMoya", package: "Moya"),
]
)
]
// CocoaPods dependency addition
// Podfile
pod 'Moya', '~> 15.0'
# For RxSwift usage
pod 'Moya/RxSwift', '~> 15.0'
# For Combine usage
pod 'Moya/Combine', '~> 15.0'
// Carthage dependency addition
// Cartfile
github "Moya/Moya" ~> 15.0
// Swift Package Manager (Xcode)
// File -> Add Package Dependencies...
// https://github.com/Moya/Moya.git
API Definition and TargetType Protocol Implementation
import Moya
import Foundation
// API endpoint definition
enum UserAPI {
case getUsers(page: Int, limit: Int)
case getUser(id: Int)
case createUser(name: String, email: String)
case updateUser(id: Int, name: String, email: String)
case deleteUser(id: Int)
case uploadAvatar(userId: Int, imageData: Data)
}
// TargetType protocol implementation
extension UserAPI: TargetType {
var baseURL: URL {
return URL(string: "https://api.example.com/v1")!
}
var path: String {
switch self {
case .getUsers:
return "/users"
case .getUser(let id), .updateUser(let id, _, _), .deleteUser(let id):
return "/users/\(id)"
case .createUser:
return "/users"
case .uploadAvatar(let userId, _):
return "/users/\(userId)/avatar"
}
}
var method: Moya.Method {
switch self {
case .getUsers, .getUser:
return .get
case .createUser:
return .post
case .updateUser:
return .put
case .deleteUser:
return .delete
case .uploadAvatar:
return .post
}
}
var sampleData: Data {
switch self {
case .getUsers:
return """
{
"users": [
{"id": 1, "name": "John Doe", "email": "[email protected]"},
{"id": 2, "name": "Jane Smith", "email": "[email protected]"}
],
"pagination": {"page": 1, "total": 2}
}
""".data(using: .utf8)!
case .getUser:
return """
{"id": 1, "name": "John Doe", "email": "[email protected]", "createdAt": "2025-01-01T00:00:00Z"}
""".data(using: .utf8)!
case .createUser, .updateUser:
return """
{"id": 1, "name": "John Doe", "email": "[email protected]", "createdAt": "2025-01-01T00:00:00Z"}
""".data(using: .utf8)!
case .deleteUser:
return Data()
case .uploadAvatar:
return """
{"avatarUrl": "https://api.example.com/avatars/user123.jpg"}
""".data(using: .utf8)!
}
}
var task: Task {
switch self {
case .getUsers(let page, let limit):
return .requestParameters(
parameters: ["page": page, "limit": limit],
encoding: URLEncoding.queryString
)
case .getUser, .deleteUser:
return .requestPlain
case .createUser(let name, let email):
let parameters = ["name": name, "email": email]
return .requestParameters(parameters: parameters, encoding: JSONEncoding.default)
case .updateUser(_, let name, let email):
let parameters = ["name": name, "email": email]
return .requestParameters(parameters: parameters, encoding: JSONEncoding.default)
case .uploadAvatar(_, let imageData):
let formData = MultipartFormData(
provider: .data(imageData),
name: "avatar",
fileName: "avatar.jpg",
mimeType: "image/jpeg"
)
return .uploadMultipart([formData])
}
}
var headers: [String: String]? {
var headers = ["Content-Type": "application/json"]
// Add auth token if available
if let token = AuthManager.shared.accessToken {
headers["Authorization"] = "Bearer \(token)"
}
return headers
}
var validationType: ValidationType {
return .successCodes
}
}
// Data model definitions
struct User: Codable {
let id: Int
let name: String
let email: String
let createdAt: Date?
enum CodingKeys: String, CodingKey {
case id, name, email
case createdAt = "created_at"
}
}
struct UsersResponse: Codable {
let users: [User]
let pagination: Pagination
}
struct Pagination: Codable {
let page: Int
let total: Int
let hasMore: Bool
enum CodingKeys: String, CodingKey {
case page, total
case hasMore = "has_more"
}
}
Provider and Request Execution
import Moya
class UserService {
// Provider initialization
private let provider = MoyaProvider<UserAPI>(
endpointClosure: { target in
let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target)
return defaultEndpoint.adding(newHTTPHeaderFields: ["User-Agent": "MyApp/1.0"])
},
requestClosure: { (endpoint, done) in
do {
var request = try endpoint.urlRequest()
request.timeoutInterval = 30.0
done(.success(request))
} catch {
done(.failure(MoyaError.underlying(error, nil)))
}
},
plugins: [
NetworkLoggerPlugin(configuration: .init(logOptions: .verbose))
]
)
// Get users list
func getUsers(page: Int = 1, limit: Int = 20, completion: @escaping (Result<UsersResponse, Error>) -> Void) {
provider.request(.getUsers(page: page, limit: limit)) { result in
switch result {
case .success(let response):
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let usersResponse = try decoder.decode(UsersResponse.self, from: response.data)
completion(.success(usersResponse))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
// Get specific user
func getUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
provider.request(.getUser(id: id)) { result in
switch result {
case .success(let response):
do {
// Response status check
if response.statusCode == 200 {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let user = try decoder.decode(User.self, from: response.data)
completion(.success(user))
} else {
let error = NSError(domain: "UserServiceError", code: response.statusCode, userInfo: [
NSLocalizedDescriptionKey: "Failed to get user"
])
completion(.failure(error))
}
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
// Create user
func createUser(name: String, email: String, completion: @escaping (Result<User, Error>) -> Void) {
provider.request(.createUser(name: name, email: email)) { result in
switch result {
case .success(let response):
do {
if response.statusCode == 201 {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let user = try decoder.decode(User.self, from: response.data)
completion(.success(user))
} else {
// Detailed error response handling
let errorResponse = try? JSONSerialization.jsonObject(with: response.data) as? [String: Any]
let message = errorResponse?["message"] as? String ?? "Failed to create user"
let error = NSError(domain: "UserServiceError", code: response.statusCode, userInfo: [
NSLocalizedDescriptionKey: message
])
completion(.failure(error))
}
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
// Update user
func updateUser(id: Int, name: String, email: String, completion: @escaping (Result<User, Error>) -> Void) {
provider.request(.updateUser(id: id, name: name, email: email)) { result in
switch result {
case .success(let response):
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let user = try decoder.decode(User.self, from: response.data)
completion(.success(user))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
// Delete user
func deleteUser(id: Int, completion: @escaping (Result<Void, Error>) -> Void) {
provider.request(.deleteUser(id: id)) { result in
switch result {
case .success(let response):
if response.statusCode == 204 {
completion(.success(()))
} else {
let error = NSError(domain: "UserServiceError", code: response.statusCode, userInfo: [
NSLocalizedDescriptionKey: "Failed to delete user"
])
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
// Usage example
class UserViewController: UIViewController {
private let userService = UserService()
override func viewDidLoad() {
super.viewDidLoad()
loadUsers()
}
private func loadUsers() {
userService.getUsers(page: 1, limit: 20) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let response):
print("User list loaded successfully: \(response.users.count) items")
// Update UI
case .failure(let error):
print("Error: \(error.localizedDescription)")
// Error handling
}
}
}
}
}
Plugin System Extension Features
import Moya
// Authentication plugin
struct AuthPlugin: PluginType {
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
var request = request
// Automatic token addition
if let token = AuthManager.shared.accessToken {
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
// API key addition
request.addValue("your-api-key", forHTTPHeaderField: "X-API-Key")
return request
}
func willSend(_ request: RequestType, target: TargetType) {
print("🚀 Request sending started: \(target)")
}
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
switch result {
case .success(let response):
print("✅ Response received successfully: \(target) - Status: \(response.statusCode)")
case .failure(let error):
print("❌ Request failed: \(target) - Error: \(error)")
}
}
}
// Cache plugin
struct CachePlugin: PluginType {
private let cache = URLCache.shared
func willSend(_ request: RequestType, target: TargetType) {
// Only cache GET requests
guard let urlRequest = request.request,
urlRequest.httpMethod == "GET" else {
return
}
// Check cached response
if let cachedResponse = cache.cachedResponse(for: urlRequest) {
let age = Date().timeIntervalSince(cachedResponse.timestamp)
if age < 300 { // Cache valid for 5 minutes
print("📦 Response retrieved from cache: \(target)")
}
}
}
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
switch result {
case .success(let response):
// Store successful response in cache
if let urlRequest = response.request,
urlRequest.httpMethod == "GET",
response.statusCode == 200 {
let cachedResponse = CachedURLResponse(response: response.response!, data: response.data)
cache.storeCachedResponse(cachedResponse, for: urlRequest)
print("💾 Response stored in cache: \(target)")
}
case .failure:
break
}
}
}
// Custom logger plugin
struct CustomLoggerPlugin: PluginType {
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return formatter
}()
func willSend(_ request: RequestType, target: TargetType) {
let timestamp = dateFormatter.string(from: Date())
print("📤 [\(timestamp)] REQUEST: \(request.request?.httpMethod ?? "UNKNOWN") \(target)")
if let headers = request.request?.allHTTPHeaderFields {
print("📋 Headers: \(headers)")
}
if let body = request.request?.httpBody {
print("📝 Body: \(String(data: body, encoding: .utf8) ?? "Binary data")")
}
}
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
let timestamp = dateFormatter.string(from: Date())
switch result {
case .success(let response):
print("📥 [\(timestamp)] RESPONSE: \(response.statusCode) \(target)")
print("📋 Headers: \(response.response?.allHeaderFields ?? [:])")
if let responseString = String(data: response.data, encoding: .utf8) {
print("📄 Body: \(responseString)")
}
case .failure(let error):
print("🚨 [\(timestamp)] ERROR: \(error) \(target)")
}
}
}
// Retry plugin
struct RetryPlugin: PluginType {
private let maxRetries: Int
private let retryDelay: TimeInterval
init(maxRetries: Int = 3, retryDelay: TimeInterval = 1.0) {
self.maxRetries = maxRetries
self.retryDelay = retryDelay
}
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
switch result {
case .success(let response):
// 5xx errors are retry candidates
if response.statusCode >= 500 {
print("🔄 Server error detected, retry candidate: \(response.statusCode)")
}
case .failure(let error):
// Network errors are retry candidates
if case .underlying(let nsError, _) = error,
(nsError as NSError).domain == NSURLErrorDomain {
print("🔄 Network error detected, retry candidate: \(nsError)")
}
}
}
}
// Enhanced provider with plugins
class EnhancedUserService {
private let provider = MoyaProvider<UserAPI>(
plugins: [
AuthPlugin(),
CachePlugin(),
CustomLoggerPlugin(),
RetryPlugin(),
NetworkLoggerPlugin(configuration: .init(
formatter: .init(responseData: { data in
return String(data: data, encoding: .utf8) ?? "Binary data"
}),
logOptions: [.requestBody, .responseBody]
))
]
)
func getUsersWithEnhancedFeatures(completion: @escaping (Result<UsersResponse, Error>) -> Void) {
provider.request(.getUsers(page: 1, limit: 20)) { result in
switch result {
case .success(let response):
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let usersResponse = try decoder.decode(UsersResponse.self, from: response.data)
completion(.success(usersResponse))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
RxSwift and Combine Reactive Extensions
import Moya
import RxSwift
import RxCocoa
import Combine
// RxSwift version service
class RxUserService {
private let provider = MoyaProvider<UserAPI>()
private let disposeBag = DisposeBag()
// Methods returning Observable
func getUsers(page: Int = 1, limit: Int = 20) -> Observable<UsersResponse> {
return provider.rx
.request(.getUsers(page: page, limit: limit))
.filterSuccessfulStatusCodes()
.map(UsersResponse.self, using: JSONDecoder.iso8601)
.asObservable()
}
func getUser(id: Int) -> Observable<User> {
return provider.rx
.request(.getUser(id: id))
.filterSuccessfulStatusCodes()
.map(User.self, using: JSONDecoder.iso8601)
.asObservable()
}
func createUser(name: String, email: String) -> Observable<User> {
return provider.rx
.request(.createUser(name: name, email: email))
.filterSuccessfulStatusCodes()
.map(User.self, using: JSONDecoder.iso8601)
.asObservable()
}
// Complex operation example: Create user → Get details
func createAndFetchUser(name: String, email: String) -> Observable<User> {
return createUser(name: name, email: email)
.flatMapLatest { user in
self.getUser(id: user.id)
}
}
// With error handling
func getUsersWithErrorHandling(page: Int = 1) -> Observable<UsersResponse> {
return getUsers(page: page)
.retry(3)
.catchAndReturn(UsersResponse(users: [], pagination: Pagination(page: 1, total: 0, hasMore: false)))
}
}
// Combine version service
@available(iOS 13.0, *)
class CombineUserService {
private let provider = MoyaProvider<UserAPI>()
private var cancellables = Set<AnyCancellable>()
// Methods returning Publisher
func getUsers(page: Int = 1, limit: Int = 20) -> AnyPublisher<UsersResponse, Error> {
return provider.requestPublisher(.getUsers(page: page, limit: limit))
.filterSuccessfulStatusCodes()
.map(UsersResponse.self, using: JSONDecoder.iso8601)
.eraseToAnyPublisher()
}
func getUser(id: Int) -> AnyPublisher<User, Error> {
return provider.requestPublisher(.getUser(id: id))
.filterSuccessfulStatusCodes()
.map(User.self, using: JSONDecoder.iso8601)
.eraseToAnyPublisher()
}
func createUser(name: String, email: String) -> AnyPublisher<User, Error> {
return provider.requestPublisher(.createUser(name: name, email: email))
.filterSuccessfulStatusCodes()
.map(User.self, using: JSONDecoder.iso8601)
.eraseToAnyPublisher()
}
// Complex operation example: Parallel fetching of multiple users
func getMultipleUsers(ids: [Int]) -> AnyPublisher<[User], Error> {
let publishers = ids.map { getUser(id: $0) }
return Publishers.MergeMany(publishers)
.collect()
.eraseToAnyPublisher()
}
// Real-time data update example
func startUserListUpdates() -> AnyPublisher<UsersResponse, Error> {
return Timer.publish(every: 30.0, on: .main, in: .common)
.autoconnect()
.flatMap { _ in
self.getUsers(page: 1, limit: 20)
}
.eraseToAnyPublisher()
}
}
// ViewModel usage example (RxSwift)
class RxUserListViewModel {
private let userService = RxUserService()
private let disposeBag = DisposeBag()
// Input
let loadTrigger = PublishSubject<Void>()
let refreshTrigger = PublishSubject<Void>()
// Output
let users = BehaviorRelay<[User]>(value: [])
let isLoading = BehaviorRelay<Bool>(value: false)
let error = PublishSubject<Error>()
init() {
let loadUsers = Observable.merge(loadTrigger, refreshTrigger)
.do(onNext: { [weak self] in
self?.isLoading.accept(true)
})
.flatMapLatest { [weak self] _ -> Observable<UsersResponse> in
guard let self = self else { return .empty() }
return self.userService.getUsers()
.catchError { error in
self.error.onNext(error)
return .empty()
}
}
.do(onNext: { [weak self] _ in
self?.isLoading.accept(false)
})
loadUsers
.map { $0.users }
.bind(to: users)
.disposed(by: disposeBag)
}
}
// ViewModel usage example (Combine)
@available(iOS 13.0, *)
class CombineUserListViewModel: ObservableObject {
private let userService = CombineUserService()
private var cancellables = Set<AnyCancellable>()
@Published var users: [User] = []
@Published var isLoading: Bool = false
@Published var errorMessage: String?
func loadUsers() {
isLoading = true
errorMessage = nil
userService.getUsers()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] response in
self?.users = response.users
}
)
.store(in: &cancellables)
}
}
// JSONDecoder extension
extension JSONDecoder {
static let iso8601: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
}
Test Support Features and Mock Implementation
import Moya
import XCTest
// Test provider
class UserServiceTests: XCTestCase {
var sut: UserService!
var mockProvider: MoyaProvider<UserAPI>!
override func setUp() {
super.setUp()
// Mock provider configuration
mockProvider = MoyaProvider<UserAPI>(
endpointClosure: { target in
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
},
stubClosure: MoyaProvider.immediatelyStub
)
sut = UserService(provider: mockProvider)
}
override func tearDown() {
sut = nil
mockProvider = nil
super.tearDown()
}
// Success case test
func testGetUsersSuccess() {
let expectation = self.expectation(description: "Get users success")
sut.getUsers { result in
switch result {
case .success(let response):
XCTAssertEqual(response.users.count, 2)
XCTAssertEqual(response.users.first?.name, "John Doe")
expectation.fulfill()
case .failure(let error):
XCTFail("Expected success, got error: \(error)")
}
}
waitForExpectations(timeout: 1.0)
}
// Error case test
func testGetUsersNetworkError() {
// Provider simulating network error
let errorProvider = MoyaProvider<UserAPI>(
endpointClosure: { target in
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkError(NSError(domain: "TestError", code: -1009, userInfo: nil)) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
},
stubClosure: MoyaProvider.immediatelyStub
)
let errorService = UserService(provider: errorProvider)
let expectation = self.expectation(description: "Get users network error")
errorService.getUsers { result in
switch result {
case .success:
XCTFail("Expected error, got success")
case .failure(let error):
XCTAssertNotNil(error)
expectation.fulfill()
}
}
waitForExpectations(timeout: 1.0)
}
// Custom response test
func testCreateUserWithCustomResponse() {
let customProvider = MoyaProvider<UserAPI>(
endpointClosure: { target in
let customData: Data
if case .createUser(let name, let email) = target {
let responseDict = [
"id": 999,
"name": name,
"email": email,
"created_at": "2025-01-01T12:00:00Z"
] as [String : Any]
customData = try! JSONSerialization.data(withJSONObject: responseDict)
} else {
customData = target.sampleData
}
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(201, customData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
},
stubClosure: MoyaProvider.immediatelyStub
)
let customService = UserService(provider: customProvider)
let expectation = self.expectation(description: "Create user with custom response")
customService.createUser(name: "Test User", email: "[email protected]") { result in
switch result {
case .success(let user):
XCTAssertEqual(user.id, 999)
XCTAssertEqual(user.name, "Test User")
XCTAssertEqual(user.email, "[email protected]")
expectation.fulfill()
case .failure(let error):
XCTFail("Expected success, got error: \(error)")
}
}
waitForExpectations(timeout: 1.0)
}
}
// Performance test
extension UserServiceTests {
func testGetUsersPerformance() {
self.measure {
let expectation = self.expectation(description: "Performance test")
sut.getUsers { _ in
expectation.fulfill()
}
waitForExpectations(timeout: 1.0)
}
}
}
// Enhanced UserService (test-compatible)
class UserService {
private let provider: MoyaProvider<UserAPI>
init(provider: MoyaProvider<UserAPI>? = nil) {
self.provider = provider ?? MoyaProvider<UserAPI>()
}
// Implementation same as previous examples...
}