api-integration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAPI 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 replayThe 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-storeOffline-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架构决策矩阵
| Factor | REST | GraphQL |
|---|---|---|
| Fixed endpoints | ✅ Best | Overkill |
| Flexible queries | Verbose | ✅ Best |
| Caching | ✅ HTTP native | Complex |
| Real-time | WebSocket addition | ✅ Subscriptions |
| Offline sync | ✅ Easier | Harder |
| Learning curve | Lower | Higher |
| 因素 | REST | GraphQL |
|---|---|---|
| 固定端点 | ✅ 最优 | 大材小用 |
| 灵活查询 | 繁琐 | ✅ 最优 |
| 缓存 | ✅ HTTP原生支持 | 复杂 |
| 实时性 | 需添加WebSocket | ✅ 内置订阅 |
| 离线同步 | ✅ 更简单 | 难度更高 |
| 学习曲线 | 低 | 高 |
Caching Strategy by Data Type
按数据类型划分的缓存策略
| Data Type | Strategy | TTL |
|---|---|---|
| App config | Aggressive | 24h |
| User profile | Validate | 5-15m |
| Feed/timeline | Short or none | 1-5m |
| Payments | None | 0 |
| Images | Aggressive | 7d |
| 数据类型 | 策略 | TTL |
|---|---|---|
| 应用配置 | 激进缓存 | 24h |
| 用户资料 | 验证缓存 | 5-15m |
| 信息流/时间线 | 短TTL或不缓存 | 1-5m |
| 支付数据 | 不缓存 | 0 |
| 图片 | 激进缓存 | 7d |
Red Flags
危险信号
| Smell | Problem | Fix |
|---|---|---|
| DTO in ViewModel | Coupling to API | Map to domain model |
| Hardcoded base URL | Can't switch env | Configuration |
| Showing DecodingError | Leaks internals | User-friendly messages |
| Loading all pages | Memory explosion | Paginate on demand |
| Offset pagination for mutable data | Duplicates/gaps | Cursor pagination |
| Caching auth responses | Security risk | Exclude sensitive |
| 问题 | 影响 | 修复方案 |
|---|---|---|
| ViewModel中出现DTO | 与API耦合 | 映射为领域模型 |
| 硬编码基础URL | 无法切换环境 | 使用配置管理 |
| 显示DecodingError | 暴露内部细节 | 使用用户友好提示 |
| 一次性加载所有页面 | 内存爆炸 | 按需分页 |
| 可变数据使用偏移分页 | 重复/数据缺失 | 使用游标分页 |
| 缓存认证响应 | 安全风险 | 排除敏感端点 |