--- alwaysApply: false --- # API Authoring Guidelines (VibeviewerAPI) ## Goals - Unify API naming, directories, abstractions, dependency injection, and decoding patterns - Keep all APIs in a single module `VibeviewerAPI` to enforce isolation and modularity - Standardize `DecodableTargetType` and the `HttpClient.decodableRequest(_:)` usage (async/await only) on top of Moya/Alamofire ## Hard rules - API targets must be declared with `struct` (no `enum`/case-style targets) - Use async/await-only decoding; callback-based styles are forbidden - Separate API declarations from model declarations: - API Targets/Services → `VibeviewerAPI` - Data models/aggregations → `VibeviewerModel` - Views/upper layers must use `Service` protocols via dependency injection, and must not call API targets or `HttpClient` directly - The API module only exposes `Service` protocols and default implementations; API targets, networking details, and common header configuration remain internal ## Dependencies & imports - API module imports only: - `Foundation` - `Moya` - `Alamofire` (used via `HttpClient`) - `VibeviewerModel` - Never import UI frameworks in the API module (`SwiftUI`/`AppKit`/`UIKit`) ## Naming conventions - Targets: Feature name + `API`, e.g., `YourFeatureAPI` - Protocols: `YourFeatureService` - Default implementations: `DefaultYourFeatureService` - Models: `YourFeatureResponse`, `YourFeatureDetail`, etc. ## Directory structure (VibeviewerAPI) ```text VibeviewerAPI/ Sources/VibeviewerAPI/ Mapping/ ... DTOs & Mappers Plugins/ RequestHeaderConfigurationPlugin.swift RequestErrorHandlingPlugin.swift SimpleNetworkLoggerPlugin.swift Service/ MoyaProvider+DecodableRequest.swift HttpClient.swift # Unified Moya provider & session wrapper HttpClientError.swift Targets/ CursorGetMeAPI.swift # internal target CursorUsageAPI.swift # internal target CursorTeamSpendAPI.swift # internal target CursorService.swift # public protocol + default implementation (service only) ``` ## Target and decoding conventions - Targets conform to `DecodableTargetType`: - `associatedtype ResultType: Decodable` - `var decodeAtKeyPath: String? { get }` (default `nil`) - Implement `baseURL`, `path`, `method`, `task`, `headers`, `sampleData` - Avoid overriding `validationType` unless necessary Example: ```swift import Foundation import Moya import VibeviewerModel struct UserProfileDetailAPI: DecodableTargetType { typealias ResultType = UserProfileResponse let userId: String var baseURL: URL { APIConfig.baseURL } var path: String { "/users/\(userId)" } var method: Moya.Method { .get } var task: Task { .requestPlain } var headers: [String: String]? { APIHeadersBuilder.basicHeaders(cookieHeader: nil) } var sampleData: Data { Data("{\"id\":\"1\",\"name\":\"foo\"}".utf8) } } ``` ## Service abstraction & dependency injection - Expose protocol + default implementation (expose services only; hide networking details) - The default `public init(decoding:)` must not leak internal protocol types; provide `internal init(network:decoding:)` for test injection ```swift import Foundation import Moya import VibeviewerModel public protocol UserProfileService { func fetchDetail(userId: String) async throws -> UserProfileResponse } public struct DefaultUserProfileService: UserProfileService { private let network: NetworkClient private let decoding: JSONDecoder.KeyDecodingStrategy // Business-facing: do not expose internal NetworkClient abstraction public init(decoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) { self.network = DefaultNetworkClient() self.decoding = decoding } // Test injection: available within the API module (same package or @testable) init(network: any NetworkClient, decoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) { self.network = network self.decoding = decoding } public func fetchDetail(userId: String) async throws -> UserProfileResponse { try await network.decodableRequest( UserProfileDetailAPI(userId: userId), decodingStrategy: decoding ) } } ``` > Note: `DefaultNetworkClient`, the `NetworkClient` protocol, and the concrete `HttpClient` implementation details remain `internal` and are not exposed. ## View usage (dependency injection) Views must not call API targets or `HttpClient` directly. Use injected services instead: ```swift import VibeviewerAPI import VibeviewerModel let service: UserProfileService = DefaultUserProfileService() let model = try await service.fetchDetail(userId: "1") ``` ## Error handling & logging - Enable `SimpleNetworkLoggerPlugin` by default to log requests/responses - Enable `RequestErrorHandlingPlugin` by default: - Timeouts/offline → unified handling - Customizable via strategy protocols ## Testing & mock conventions - Within the `VibeviewerAPI` module, inject a `FakeNetworkClient` via `internal init(network:decoding:)` to replace real networking - Provide `sampleData` for each target; prefer minimal realistic JSON to ensure robust decoding - Use `@testable import VibeviewerAPI` to access internal symbols when external tests are required ## Alignment with modular architecture (architecture.mdc) - Do not import UI frameworks in the API module - Expose only `Service` protocols and default implementations; hide targets and networking details - Dependency direction: `VibeviewerModel` ← `VibeviewerAPI` ← `VibeviewerFeature` - Strict “one file, one type/responsibility”; clear feature aggregation; one-way dependencies