APIClientとは
APIClientとは、APIを呼び出す役割を持ったコンポーネントである。リクエストの送信からレスポンスの受け取りまでの一連の処理を行い、その結果を提供する。
今回はプロトコルやジェネリクスなどを利用した、様々な種類のリクエスト、レスポンスに対応する汎用的なAPIClientの作成方法を紹介する。以下は記事情報を取得できるAPIを利用するというシチュエーションを想定した例である。
コード全文
// ① エラーを表すenum
enum APIClientError: Error {
case invalidURL
case encodingError
case decodingError
case invalidResponse
case serverError(statusCode: Int)
case networkError(Error)
}
// ② 各リクエストが満たすべき要件を示すprotocol
protocol Requestable {
associatedtype Response: Decodable
associatedtype HTTPBody: Encodable
var baseURL: String { get }
var path: String { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var body: HTTPBody? { get }
}
// ③ HTTPMethodを表すenum
enum HTTPMethod: String {
case post = "POST"
}
// ④ Requestableに準拠した具体的なリクエストstruct
struct ArticleRequest: Requestable {
typealias Response = ArticleResponseBody
typealias HTTPBody = ArticleRequestBody
let baseURL: String = "https://example.com"
let path: String = "/article"
let method: HTTPMethod = .post
let headers: [String: String]? = [
"Content-Type": "application/json"
]
let body: ArticleRequestBody?
}
// ⑤ ArticleRequestのBodyの内容を表すstruct
struct ArticleRequestBody: Encodable {
let articleId: String
let includeContent: Bool
}
// ⑥ ArticleRequestのレスポンスデータのデコード形式を表すstruct
struct ArticleResponseBody: Decodable {
let title: String
let content: String
let publishedAt: Date
}
// ⑦ API通信を担当するclass
class APIClient {
private let urlSession: URLSession
init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
func request<T: Requestable>(_ request: T) async throws -> T.Response {
guard let url = URL(string: request.baseURL + request.path) else {
throw APIClientError.invalidURL
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = request.method.rawValue
request.headers?.forEach {
urlRequest.setValue($1, forHTTPHeaderField: $0)
}
if let body = request.body {
do {
urlRequest.httpBody = try JSONEncoder().encode(body)
} catch {
throw APIClientError.encodingError
}
}
do {
let (data, response) = try await urlSession.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIClientError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw APIClientError.serverError(statusCode: httpResponse.statusCode)
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(T.Response.self, from: data)
} catch let error as APIClientError {
throw error
} catch {
throw APIClientError.networkError(error)
}
}
}
使用例
let client = APIClient()
let request = ArticleRequest(
body: ArticleRequestBody(
articleId: "123",
includeContent: true
)
)
Task {
do {
let response = try await client.request(request)
print(response.title)
print(response.content)
print(response.publishedAt)
} catch {
print(error)
}
}
各要素の解説
① APIClientError
enum APIClientError: Error {
case invalidURL
case encodingError
case decodingError
case invalidResponse
case serverError(statusCode: Int)
case networkError(Error)
}
- APIClientで発生する可能性があるエラーを表現するenum。
- Errorに準拠させ、throwで投げられるようにする。
② Requestable
protocol Requestable {
associatedtype Response: Decodable
associatedtype HTTPBody: Encodable
var baseURL: String { get }
var path: String { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var body: HTTPBody? { get }
}
- リクエストが満たすべき要件を定義するprotocol。今回作成するAPIClientで送信する個別のリクエスト達は必ずこのprotocolに準拠させる必要がある。
- Response: DecodableとHTTPBody: Encodableという二つのassociatedtypeを持つ。このprotocolに準拠した各リクエストで具体的なリクエストBodyとレスポンスBodyの型を指定する(④ ArticleRequestを参照)
③ HTTPMethod
enum HTTPMethod: String {
case post = "POST"
}
- 扱うHTTPMethodの種類を管理するenum。
④ ArticleRequest
struct ArticleRequest: Requestable {
typealias Response = ArticleResponseBody
typealias HTTPBody = ArticleRequestBody
let baseURL: String = "https://example.com"
let path: String = "/article"
let method: HTTPMethod = .post
let headers: [String: String]? = [
"Content-Type": "application/json"
]
let body: ArticleRequestBody?
}
- 記事取得のためのリクエストを表現するstruct。
- Requestableに準拠しており、このリクエストで行うAPI通信で扱いたいResponseとHTTPBodyの具体的な型を指定している。
⑤ ArticleRequestBody
struct ArticleRequestBody: Encodable {
let articleId: String
let includeContent: Bool
}
- 記事取得リクエストのBodyの内容を表すstruct。
- ArticleRequestのHTTPBodyのtypealiasに指定するためにEncodableに準拠させる。
⑥ ArticleResponseBody
struct ArticleResponseBody: Decodable {
let title: String
let content: String
let publishedAt: Date
}
- 記事取得のリクエストに対するレスポンスのデコード形式を表すstruct。
- ArticleRequestのResponseのtypealiasに指定するためにDecodableに準拠させる。
⑦ APIClient
class APIClient {
private let urlSession: URLSession
init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
func request<T: Requestable>(_ request: T) async throws -> T.Response {
guard let url = URL(string: request.baseURL + request.path) else {
throw APIClientError.invalidURL
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = request.method.rawValue
request.headers?.forEach {
urlRequest.setValue($1, forHTTPHeaderField: $0)
}
if let body = request.body {
do {
urlRequest.httpBody = try JSONEncoder().encode(body)
} catch {
throw APIClientError.encodingError
}
}
do {
let (data, response) = try await urlSession.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIClientError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw APIClientError.serverError(statusCode: httpResponse.statusCode)
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(T.Response.self, from: data)
} catch let error as APIClientError {
throw error
} catch {
throw APIClientError.networkError(error)
}
}
}
- リクエスト作成、APIとの通信、レスポンスデータのデコードまでを行うclass。
- requestはジェネリック関数であり、Requestableを制約とした型引数Tを定義している。受け取るリクエストの型をTとしているため、このメソッドはRequestableに準拠していればどんなリクエストでも引数で受け取ることができる。今回の例で言えば、ArticleRequestが渡される。
- そして、戻り値には受け取ったリクエストに設定されているResponseの具体型を指定している。こちらも今回の例で言えば、ArticleResponseBodyが具体型となる。