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.

HTTP ClientSwiftiOSType SafeNetwork AbstractionReactive

GitHub Overview

Moya/Moya

Network abstraction layer written in Swift.

Stars15,298
Watchers245
Forks2,009
Created:August 16, 2014
Language:Swift
License:MIT License

Topics

alamofirehacktoberfestnetworkingreactiveswiftrxswiftswift

Star History

Moya/Moya Star History
Data as of: 7/18/2025, 01:38 AM

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...
}