api integration

API Integration — Expert Decisions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "api integration" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-api-integration

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rails localization (i18n) - english & arabic

No summary provided by upstream source.

Repository SourceNeeds Review