1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Swift】使いまわせるAPIClientの作成方法

1
Last updated at Posted at 2026-03-22

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が具体型となる。

参考文献

[iOS] URLSessionでAPIクライアントを実装する
ジェネリクス(Generics)

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?