api-integration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

API Integration — Expert Decisions

API集成——专家决策

Expert decision frameworks for API integration choices. Claude knows URLSession and Codable — this skill provides judgment calls for architecture decisions and caching strategies.

API集成选择的专家决策框架。Claude熟悉URLSession和Codable——本技能为架构决策和缓存策略提供判断依据。

Decision Trees

决策树

REST vs GraphQL

REST与GraphQL

What's your data access pattern?
├─ Fixed, well-defined endpoints
│  └─ REST
│     Simpler caching, HTTP semantics
├─ Flexible queries, varying data needs per screen
│  └─ GraphQL
│     Single endpoint, client specifies shape
├─ Real-time subscriptions needed?
│  └─ GraphQL subscriptions or WebSocket + REST
│     GraphQL has built-in subscription support
└─ Offline-first with sync?
   └─ REST is simpler for conflict resolution
      GraphQL mutations harder to replay
The trap: Choosing GraphQL because it's trendy. If your API has stable endpoints and you control both client and server, REST is simpler.
你的数据访问模式是什么?
├─ 固定、定义明确的端点
│  └─ REST
│     缓存更简单,遵循HTTP语义
├─ 灵活查询,各界面数据需求不同
│  └─ GraphQL
│     单一端点,客户端指定数据结构
├─ 需要实时订阅?
│  └─ GraphQL订阅或WebSocket + REST
│     GraphQL内置订阅支持
└─ 需支持离线优先同步?
   └─ REST的冲突解决更简单
      GraphQL突变重放难度更高
误区:因为GraphQL流行就选择它。如果你的API端点稳定,且你同时控制客户端和服务器,REST会更简单。

API Versioning Strategy

API版本控制策略

How stable is your API?
├─ Stable, rarely changes
│  └─ No versioning needed initially
│     Add when first breaking change occurs
├─ Breaking changes expected
│  └─ URL path versioning (/v1/, /v2/)
│     Most explicit, easiest to manage
├─ Gradual migration needed
│  └─ Header versioning (Accept-Version: v2)
│     Same URL, version negotiated
└─ Multiple versions simultaneously
   └─ Consider if you really need this
      Maintenance burden is high
你的API稳定性如何?
├─ 稳定,极少变更
│  └─ 初期无需版本控制
│     首次出现破坏性变更时再添加
├─ 预计会有破坏性变更
│  └─ URL路径版本化(/v1/、/v2/)
│     最明确,最易管理
├─ 需要逐步迁移
│  └─ 请求头版本化(Accept-Version: v2)
│     同一URL,协商版本
└─ 需同时维护多个版本
   └─ 先考虑是否真的需要
      维护负担极高

Caching Strategy Selection

缓存策略选择

What type of data?
├─ Static/rarely changes (images, config)
│  └─ Aggressive cache (URLCache + ETag)
│     Cache-Control: max-age=86400
├─ User-specific, changes occasionally
│  └─ Cache with validation (ETag/Last-Modified)
│     Always validate, but use cached if unchanged
├─ Frequently changing (feeds, notifications)
│  └─ No cache or short TTL
│     Cache-Control: no-cache or max-age=60
└─ Critical real-time data (payments, inventory)
   └─ No cache, always fetch
      Cache-Control: no-store
数据类型是什么?
├─ 静态/极少变更(图片、配置)
│  └─ 激进缓存(URLCache + ETag)
│     Cache-Control: max-age=86400
├─ 用户专属,偶尔变更
│  └─ 带验证的缓存(ETag/Last-Modified)
│     始终验证,若未变更则使用缓存
├─ 频繁变更(信息流、通知)
│  └─ 不缓存或短TTL
│     Cache-Control: no-cache或max-age=60
└─ 关键实时数据(支付、库存)
   └─ 不缓存,始终获取
      Cache-Control: no-store

Offline-First Architecture

离线优先架构

How important is offline?
├─ Nice to have (can show empty state)
│  └─ Simple memory cache
│     Clear on app restart
├─ Must show stale data when offline
│  └─ Persistent cache (Core Data, Realm, SQLite)
│     Fetch fresh when online, fallback to cache
├─ Must sync user changes when back online
│  └─ Full offline-first architecture
│     Local-first writes, sync queue, conflict resolution
└─ Real-time collaboration
   └─ Consider CRDTs or operational transforms
      Complex — usually overkill for mobile

离线支持的重要性如何?
├─ 锦上添花(可显示空状态)
│  └─ 简单内存缓存
│     应用重启时清空
├─ 离线时必须显示过期数据
│  └─ 持久化缓存(Core Data、Realm、SQLite)
│     在线时获取最新数据,离线时回退到缓存
├─ 恢复在线后必须同步用户变更
│  └─ 完整离线优先架构
│     本地优先写入、同步队列、冲突解决
└─ 实时协作
   └─ 考虑CRDT或操作转换
      复杂度高——通常对移动应用来说没必要

NEVER Do

绝对禁忌

Service Layer Design

服务层设计

NEVER call NetworkManager directly from ViewModels:
swift
// ❌ ViewModel knows about network layer
@MainActor
final class UserViewModel: ObservableObject {
    func loadUser() async {
        let response = try await NetworkManager.shared.request(APIRouter.getUser(id: "123"))
        // ViewModel parsing network response directly
    }
}

// ✅ Service layer abstracts network details
@MainActor
final class UserViewModel: ObservableObject {
    private let userService: UserServiceProtocol

    func loadUser() async {
        user = try await userService.getUser(id: "123")
        // ViewModel works with domain models
    }
}
NEVER expose DTOs beyond service layer:
swift
// ❌ DTO leaks to ViewModel
func getUser() async throws -> UserDTO {
    try await networkManager.request(...)
}

// ✅ Service maps DTO to domain model
func getUser() async throws -> User {
    let dto: UserDTO = try await networkManager.request(...)
    return User(from: dto)  // Mapping happens here
}
NEVER hardcode base URLs:
swift
// ❌ Can't change per environment
static let baseURL = "https://api.production.com"

// ✅ Environment-driven configuration
static var baseURL: String {
    #if DEBUG
    return "https://api.staging.com"
    #else
    return "https://api.production.com"
    #endif
}

// Better: Inject via configuration
绝对不要直接在ViewModel中调用NetworkManager:
swift
// ❌ ViewModel耦合到网络层
@MainActor
final class UserViewModel: ObservableObject {
    func loadUser() async {
        let response = try await NetworkManager.shared.request(APIRouter.getUser(id: "123"))
        // ViewModel直接解析网络响应
    }
}

// ✅ 服务层抽象网络细节
@MainActor
final class UserViewModel: ObservableObject {
    private let userService: UserServiceProtocol

    func loadUser() async {
        user = try await userService.getUser(id: "123")
        // ViewModel处理领域模型
    }
}
绝对不要让DTO暴露到服务层之外:
swift
// ❌ DTO泄露到ViewModel
func getUser() async throws -> UserDTO {
    try await networkManager.request(...)
}

// ✅ 服务层将DTO映射为领域模型
func getUser() async throws -> User {
    let dto: UserDTO = try await networkManager.request(...)
    return User(from: dto)  // 映射操作在此处完成
}
绝对不要硬编码基础URL:
swift
// ❌ 无法根据环境切换
static let baseURL = "https://api.production.com"

// ✅ 环境驱动的配置
static var baseURL: String {
    #if DEBUG
    return "https://api.staging.com"
    #else
    return "https://api.production.com"
    #endif
}

// 更优方案:通过配置注入

Error Handling

错误处理

NEVER show raw error messages to users:
swift
// ❌ Exposes technical details
errorMessage = error.localizedDescription  // "JSON decoding error at keyPath..."

// ✅ Map to user-friendly messages
errorMessage = mapToUserMessage(error)

func mapToUserMessage(_ error: Error) -> String {
    switch error {
    case let networkError as NetworkError:
        return networkError.userMessage
    case is DecodingError:
        return "Unable to process response. Please try again."
    default:
        return "Something went wrong. Please try again."
    }
}
NEVER treat all errors the same:
swift
// ❌ Same handling for all errors
catch {
    showErrorAlert(error.localizedDescription)
}

// ✅ Different handling per error type
catch is CancellationError {
    return  // User navigated away — not an error
}
catch NetworkError.unauthorized {
    await sessionManager.logout()  // Force re-auth
}
catch NetworkError.noConnection {
    showOfflineBanner()  // Different UI treatment
}
catch {
    showErrorAlert("Something went wrong")
}
绝对不要向用户显示原始错误信息:
swift
// ❌ 暴露技术细节
errorMessage = error.localizedDescription  // "JSON解码错误,路径为..."

// ✅ 映射为用户友好的提示
errorMessage = mapToUserMessage(error)

func mapToUserMessage(_ error: Error) -> String {
    switch error {
    case let networkError as NetworkError:
        return networkError.userMessage
    case is DecodingError:
        return "无法处理响应,请重试。"
    default:
        return "出现错误,请重试。"
    }
}
绝对不要统一处理所有错误:
swift
// ❌ 所有错误处理逻辑相同
catch {
    showErrorAlert(error.localizedDescription)
}

// ✅ 根据错误类型区别处理
catch is CancellationError {
    return  // 用户已离开——不视为错误
}
catch NetworkError.unauthorized {
    await sessionManager.logout()  // 强制重新认证
}
catch NetworkError.noConnection {
    showOfflineBanner()  // 不同的UI处理
}
catch {
    showErrorAlert("出现错误,请重试。")
}

Caching

缓存

NEVER cache sensitive data without encryption:
swift
// ❌ Tokens cached in plain URLCache
URLCache.shared.storeCachedResponse(response, for: request)
// Login response with tokens now on disk!

// ✅ Exclude sensitive endpoints from cache
if endpoint.isSensitive {
    request.cachePolicy = .reloadIgnoringLocalCacheData
}
NEVER assume cached data is fresh:
swift
// ❌ Trusts cache blindly
if let cached = cache.get(key) {
    return cached  // May be hours/days old
}

// ✅ Validate cache or show stale indicator
if let cached = cache.get(key) {
    if cached.isStale {
        Task { await refreshInBackground(key) }
    }
    return (data: cached.data, isStale: cached.isStale)
}
绝对不要未加密就缓存敏感数据:
swift
// ❌ 令牌以明文形式缓存到URLCache
URLCache.shared.storeCachedResponse(response, for: request)
// 包含令牌的登录响应现在存储在磁盘上!

// ✅ 将敏感端点排除在缓存之外
if endpoint.isSensitive {
    request.cachePolicy = .reloadIgnoringLocalCacheData
}
绝对不要假设缓存数据是最新的:
swift
// ❌ 盲目信任缓存
if let cached = cache.get(key) {
    return cached  // 可能已过期数小时/数天
}

// ✅ 验证缓存或显示过期标识
if let cached = cache.get(key) {
    if cached.isStale {
        Task { await refreshInBackground(key) }
    }
    return (data: cached.data, isStale: cached.isStale)
}

Pagination

分页

NEVER load all pages at once:
swift
// ❌ Memory explosion, slow initial load
func loadAllUsers() async throws -> [User] {
    var all: [User] = []
    var page = 1
    while true {
        let response = try await fetchPage(page)
        all.append(contentsOf: response.users)
        if response.users.isEmpty { break }
        page += 1
    }
    return all  // May be thousands of items!
}

// ✅ Paginate on demand
func loadNextPage() async throws {
    guard hasMorePages, !isLoading else { return }
    isLoading = true
    let response = try await fetchPage(currentPage + 1)
    users.append(contentsOf: response.users)
    currentPage += 1
    hasMorePages = !response.users.isEmpty
    isLoading = false
}
NEVER use offset pagination for mutable lists:
swift
// ❌ Items shift during pagination
// User scrolls, new item added, page 2 now has duplicate
GET /users?offset=20&limit=20

// ✅ Cursor-based pagination
GET /users?after=abc123&limit=20
// Cursor is stable reference point

绝对不要一次性加载所有页面:
swift
// ❌ 内存爆炸,初始加载缓慢
func loadAllUsers() async throws -> [User] {
    var all: [User] = []
    var page = 1
    while true {
        let response = try await fetchPage(page)
        all.append(contentsOf: response.users)
        if response.users.isEmpty { break }
        page += 1
    }
    return all  // 可能包含数千条数据!
}

// ✅ 按需分页
func loadNextPage() async throws {
    guard hasMorePages, !isLoading else { return }
    isLoading = true
    let response = try await fetchPage(currentPage + 1)
    users.append(contentsOf: response.users)
    currentPage += 1
    hasMorePages = !response.users.isEmpty
    isLoading = false
}
绝对不要对可变列表使用偏移分页:
swift
// ❌ 分页过程中数据项会移位
// 用户滚动时添加了新项,第2页会出现重复数据
GET /users?offset=20&limit=20

// ✅ 基于游标分页
GET /users?after=abc123&limit=20
// 游标是稳定的参考点

Essential Patterns

核心模式

Service Layer with Caching

带缓存的服务层

swift
protocol UserServiceProtocol {
    func getUser(id: String, forceRefresh: Bool) async throws -> User
}

final class UserService: UserServiceProtocol {
    private let networkManager: NetworkManagerProtocol
    private let cache: CacheProtocol

    func getUser(id: String, forceRefresh: Bool = false) async throws -> User {
        let cacheKey = "user_\(id)"

        // Return cached if valid and not forcing refresh
        if !forceRefresh, let cached: User = cache.get(cacheKey), !cached.isExpired {
            return cached
        }

        // Fetch fresh
        let dto: UserDTO = try await networkManager.request(APIRouter.getUser(id: id))
        let user = User(from: dto)

        // Cache with TTL
        cache.set(cacheKey, value: user, ttl: .minutes(5))

        return user
    }
}
swift
protocol UserServiceProtocol {
    func getUser(id: String, forceRefresh: Bool) async throws -> User
}

final class UserService: UserServiceProtocol {
    private let networkManager: NetworkManagerProtocol
    private let cache: CacheProtocol

    func getUser(id: String, forceRefresh: Bool = false) async throws -> User {
        let cacheKey = "user_\(id)"

        // 若缓存有效且未强制刷新,则返回缓存
        if !forceRefresh, let cached: User = cache.get(cacheKey), !cached.isExpired {
            return cached
        }

        // 获取最新数据
        let dto: UserDTO = try await networkManager.request(APIRouter.getUser(id: id))
        let user = User(from: dto)

        // 带TTL的缓存
        cache.set(cacheKey, value: user, ttl: .minutes(5))

        return user
    }
}

Stale-While-Revalidate Pattern

过期时重新验证模式

swift
func getData() async -> (data: Data?, isStale: Bool) {
    // Immediately return cached (possibly stale)
    let cached = cache.get(key)

    // Refresh in background
    Task {
        do {
            let fresh = try await fetchFromNetwork()
            cache.set(key, value: fresh)
            // Notify UI of fresh data (publisher, callback, etc.)
            await MainActor.run { self.data = fresh }
        } catch {
            // Keep showing stale data
        }
    }

    return (data: cached?.data, isStale: cached?.isExpired ?? true)
}
swift
func getData() async -> (data: Data?, isStale: Bool) {
    // 立即返回缓存(可能已过期)
    let cached = cache.get(key)

    // 后台刷新
    Task {
        do {
            let fresh = try await fetchFromNetwork()
            cache.set(key, value: fresh)
            // 通知UI更新最新数据(发布者、回调等)
            await MainActor.run { self.data = fresh }
        } catch {
            // 继续显示过期数据
        }
    }

    return (data: cached?.data, isStale: cached?.isExpired ?? true)
}

Retry with Idempotency Key

带幂等键的重试机制

swift
struct CreateOrderRequest {
    let items: [OrderItem]
    let idempotencyKey: String  // Client-generated UUID
}

func createOrder(items: [OrderItem]) async throws -> Order {
    let idempotencyKey = UUID().uuidString

    return try await withRetry(maxAttempts: 3) {
        try await orderService.create(
            CreateOrderRequest(items: items, idempotencyKey: idempotencyKey)
        )
        // Server uses idempotencyKey to prevent duplicate orders
    }
}
swift
struct CreateOrderRequest {
    let items: [OrderItem]
    let idempotencyKey: String  // 客户端生成的UUID
}

func createOrder(items: [OrderItem]) async throws -> Order {
    let idempotencyKey = UUID().uuidString

    return try await withRetry(maxAttempts: 3) {
        try await orderService.create(
            CreateOrderRequest(items: items, idempotencyKey: idempotencyKey)
        )
        // 服务器使用幂等键防止重复下单
    }
}

Background Fetch Configuration

后台获取配置

swift
// In AppDelegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    Task {
        do {
            let hasNewData = try await syncService.performBackgroundSync()
            completionHandler(hasNewData ? .newData : .noData)
        } catch {
            completionHandler(.failed)
        }
    }
}

// In Info.plist: UIBackgroundModes = ["fetch"]
// Call: UIApplication.shared.setMinimumBackgroundFetchInterval(...)

swift
// 在AppDelegate中
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    Task {
        do {
            let hasNewData = try await syncService.performBackgroundSync()
            completionHandler(hasNewData ? .newData : .noData)
        } catch {
            completionHandler(.failed)
        }
    }
}

// 在Info.plist中:UIBackgroundModes = ["fetch"]
// 调用:UIApplication.shared.setMinimumBackgroundFetchInterval(...)

Quick Reference

快速参考

API Architecture Decision Matrix

API架构决策矩阵

FactorRESTGraphQL
Fixed endpoints✅ BestOverkill
Flexible queriesVerbose✅ Best
Caching✅ HTTP nativeComplex
Real-timeWebSocket addition✅ Subscriptions
Offline sync✅ EasierHarder
Learning curveLowerHigher
因素RESTGraphQL
固定端点✅ 最优大材小用
灵活查询繁琐✅ 最优
缓存✅ HTTP原生支持复杂
实时性需添加WebSocket✅ 内置订阅
离线同步✅ 更简单难度更高
学习曲线

Caching Strategy by Data Type

按数据类型划分的缓存策略

Data TypeStrategyTTL
App configAggressive24h
User profileValidate5-15m
Feed/timelineShort or none1-5m
PaymentsNone0
ImagesAggressive7d
数据类型策略TTL
应用配置激进缓存24h
用户资料验证缓存5-15m
信息流/时间线短TTL或不缓存1-5m
支付数据不缓存0
图片激进缓存7d

Red Flags

危险信号

SmellProblemFix
DTO in ViewModelCoupling to APIMap to domain model
Hardcoded base URLCan't switch envConfiguration
Showing DecodingErrorLeaks internalsUser-friendly messages
Loading all pagesMemory explosionPaginate on demand
Offset pagination for mutable dataDuplicates/gapsCursor pagination
Caching auth responsesSecurity riskExclude sensitive
问题影响修复方案
ViewModel中出现DTO与API耦合映射为领域模型
硬编码基础URL无法切换环境使用配置管理
显示DecodingError暴露内部细节使用用户友好提示
一次性加载所有页面内存爆炸按需分页
可变数据使用偏移分页重复/数据缺失使用游标分页
缓存认证响应安全风险排除敏感端点