api-integration
SKILL.md
API Integration — Expert Decisions
Expert decision frameworks for API integration choices. Claude knows URLSession and Codable — this skill provides judgment calls for architecture decisions and caching strategies.
Decision Trees
REST vs 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.
API Versioning Strategy
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
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
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
NEVER Do
Service Layer Design
NEVER call NetworkManager directly from ViewModels:
// ❌ 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:
// ❌ 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:
// ❌ 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
Error Handling
NEVER show raw error messages to users:
// ❌ 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:
// ❌ 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")
}
Caching
NEVER cache sensitive data without encryption:
// ❌ 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:
// ❌ 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)
}
Pagination
NEVER load all pages at once:
// ❌ 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:
// ❌ 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
Essential Patterns
Service Layer with Caching
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
}
}
Stale-While-Revalidate Pattern
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)
}
Retry with Idempotency Key
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
}
}
Background Fetch Configuration
// 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(...)
Quick Reference
API Architecture Decision Matrix
| 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 |
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 |
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 |
Weekly Installs
15
Repository
kaakati/rails-e…rise-devGitHub Stars
6
First Seen
Jan 25, 2026
Security Audits
Installed on
opencode13
claude-code12
gemini-cli12
codex12
github-copilot11
antigravity10