Handle networking in Swift - URLSession, async/await, Codable, API clients, error handling
Builds type-safe API clients with async/await, Codable, and automatic retry logic.
/plugin marketplace add pluginagentmarketplace/custom-plugin-swift/plugin install swift-assistant@pluginagentmarketplace-swiftThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/APIClient.swift.templateassets/config.yamlreferences/GUIDE.mdscripts/generate-mock.shModern networking patterns for Swift applications using URLSession, async/await, and type-safe API clients.
parameters:
base_url:
type: string
required: true
description: API base URL
timeout:
type: number
default: 30
description: Request timeout in seconds
retry_enabled:
type: boolean
default: true
max_retries:
type: number
default: 3
auth_type:
type: string
enum: [none, bearer, api_key, basic]
default: bearer
| Configuration | Use Case |
|---|---|
.default | Standard caching, credentials |
.ephemeral | No persistent storage |
.background | Large transfers, app suspended |
| Method | Purpose | Body |
|---|---|---|
| GET | Retrieve resource | No |
| POST | Create resource | Yes |
| PUT | Replace resource | Yes |
| PATCH | Partial update | Yes |
| DELETE | Remove resource | Optional |
| Status Code | Meaning | Action |
|---|---|---|
| 200-299 | Success | Parse response |
| 400 | Bad Request | Validation error |
| 401 | Unauthorized | Refresh token / re-auth |
| 403 | Forbidden | Permission denied |
| 404 | Not Found | Resource missing |
| 429 | Rate Limited | Backoff and retry |
| 500-599 | Server Error | Retry with backoff |
import Foundation
// MARK: - Protocol Definitions
protocol APIEndpoint {
associatedtype Response: Decodable
var path: String { get }
var method: HTTPMethod { get }
var headers: [String: String] { get }
var queryItems: [URLQueryItem]? { get }
var body: Encodable? { get }
}
extension APIEndpoint {
var headers: [String: String] { [:] }
var queryItems: [URLQueryItem]? { nil }
var body: Encodable? { nil }
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
// MARK: - API Client
actor APIClient {
private let session: URLSession
private let baseURL: URL
private let decoder: JSONDecoder
private let encoder: JSONEncoder
private var authToken: String?
init(baseURL: URL, configuration: URLSessionConfiguration = .default) {
self.baseURL = baseURL
self.session = URLSession(configuration: configuration)
self.decoder = JSONDecoder()
self.decoder.dateDecodingStrategy = .iso8601
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
self.encoder = JSONEncoder()
self.encoder.dateEncodingStrategy = .iso8601
self.encoder.keyEncodingStrategy = .convertToSnakeCase
}
func setAuthToken(_ token: String?) {
self.authToken = token
}
func request<E: APIEndpoint>(_ endpoint: E) async throws -> E.Response {
let request = try buildRequest(for: endpoint)
let (data, response) = try await session.data(for: request)
try validateResponse(response, data: data)
do {
return try decoder.decode(E.Response.self, from: data)
} catch {
throw APIError.decodingError(error, data: data)
}
}
private func buildRequest<E: APIEndpoint>(for endpoint: E) throws -> URLRequest {
var components = URLComponents(url: baseURL.appendingPathComponent(endpoint.path), resolvingAgainstBaseURL: true)!
components.queryItems = endpoint.queryItems
guard let url = components.url else {
throw APIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = endpoint.method.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
if let token = authToken {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
endpoint.headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
if let body = endpoint.body {
request.httpBody = try encoder.encode(AnyEncodable(body))
}
return request
}
private func validateResponse(_ response: URLResponse, data: Data) throws {
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
switch httpResponse.statusCode {
case 200..<300:
return
case 401:
throw APIError.unauthorized
case 403:
throw APIError.forbidden
case 404:
throw APIError.notFound
case 429:
throw APIError.rateLimited
case 400..<500:
throw APIError.clientError(statusCode: httpResponse.statusCode, data: data)
case 500..<600:
throw APIError.serverError(statusCode: httpResponse.statusCode)
default:
throw APIError.unknown(statusCode: httpResponse.statusCode)
}
}
}
// MARK: - Error Types
enum APIError: LocalizedError {
case invalidURL
case invalidResponse
case unauthorized
case forbidden
case notFound
case rateLimited
case clientError(statusCode: Int, data: Data)
case serverError(statusCode: Int)
case decodingError(Error, data: Data)
case unknown(statusCode: Int)
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case .invalidResponse: return "Invalid response"
case .unauthorized: return "Authentication required"
case .forbidden: return "Access denied"
case .notFound: return "Resource not found"
case .rateLimited: return "Rate limit exceeded"
case .clientError(let code, _): return "Client error: \(code)"
case .serverError(let code): return "Server error: \(code)"
case .decodingError(let error, _): return "Decoding failed: \(error)"
case .unknown(let code): return "Unknown error: \(code)"
}
}
var isRetryable: Bool {
switch self {
case .rateLimited, .serverError: return true
default: return false
}
}
}
extension APIClient {
func requestWithRetry<E: APIEndpoint>(
_ endpoint: E,
maxRetries: Int = 3,
initialDelay: Duration = .seconds(1)
) async throws -> E.Response {
var lastError: Error?
var delay = initialDelay
for attempt in 0..<maxRetries {
do {
return try await request(endpoint)
} catch let error as APIError where error.isRetryable {
lastError = error
if attempt < maxRetries - 1 {
try await Task.sleep(for: delay)
delay *= 2 // Exponential backoff
}
} catch {
throw error // Non-retryable error
}
}
throw lastError ?? APIError.unknown(statusCode: 0)
}
}
enum ProductsAPI {
struct GetProducts: APIEndpoint {
typealias Response = [Product]
let path = "/products"
let method = HTTPMethod.get
var queryItems: [URLQueryItem]? {
[URLQueryItem(name: "page", value: "\(page)")]
}
let page: Int
}
struct CreateProduct: APIEndpoint {
typealias Response = Product
let path = "/products"
let method = HTTPMethod.post
var body: Encodable? { request }
let request: CreateProductRequest
}
struct DeleteProduct: APIEndpoint {
typealias Response = EmptyResponse
var path: String { "/products/\(id)" }
let method = HTTPMethod.delete
let id: String
}
}
struct EmptyResponse: Decodable {}
// Usage
let client = APIClient(baseURL: URL(string: "https://api.example.com")!)
await client.setAuthToken("your-token")
let products = try await client.request(ProductsAPI.GetProducts(page: 1))
let newProduct = try await client.request(ProductsAPI.CreateProduct(request: .init(name: "Widget")))
| Issue | Cause | Solution |
|---|---|---|
| "The certificate is not trusted" | SSL pinning or self-signed | Configure ATS or add certificate |
| "keyNotFound" | JSON key mismatch | Check CodingKeys, use keyDecodingStrategy |
| Request timeout | Network or server slow | Increase timeout, add retry |
| Memory spike on download | Not streaming | Use URLSession download task |
// Log request/response
extension URLRequest {
func log() {
print("➡️ \(httpMethod ?? "?") \(url?.absoluteString ?? "?")")
if let body = httpBody, let str = String(data: body, encoding: .utf8) {
print("Body: \(str)")
}
}
}
// Pretty print JSON for debugging
extension Data {
var prettyJSON: String {
guard let object = try? JSONSerialization.jsonObject(with: self),
let data = try? JSONSerialization.data(withJSONObject: object, options: .prettyPrinted),
let string = String(data: data, encoding: .utf8) else {
return String(data: self, encoding: .utf8) ?? "Invalid data"
}
return string
}
}
validation:
- rule: use_async_await
severity: info
check: Prefer async/await over completion handlers
- rule: handle_all_errors
severity: error
check: All network calls must have error handling
- rule: no_hardcoded_urls
severity: warning
check: URLs should be configurable, not hardcoded
Skill("swift-networking")
swift-concurrency - Async patternsswift-core-data - Caching responsesswift-testing - Mocking network callsThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.