【Ruby】APIエンドポイントの接続をモジュール化する 〜QiitaAPIを例に添えて〜 

APIエンドポイントに接続して結果を取得したい！
そんなとき、皆さんはどのようにコーディングしていますか？


APIを使用するときって、アプリケーション自体のロジックとは別にさまざまな設定用のコードを書く必要があります。
そのため何も意識せずに手続き型でコーディングすると、ついついコードの見通しが悪くなってしまいがちです。

また、アプリケーション自体のロジックとAPI用のロジックを同じメソッド内で書いてしまうと、API用のロジックだけ別の部分で再利用したいときに困ってしまいます。

そんなコード、皆さんもきっと嫌ですよね？

なので今回はQiitaAPIを例にしながら、APIエンドポイントをモジュールとして切り出して再利用しやすくする方法を紹介していきます。

# 1. この記事の目標
手続き型で書かれたAPIエンドポイントの接続メソッドをモジュール化することで、アプリケーション自体のロジックとエンドポイント接続のためのロジックを分離することが目標です。

ロジックを分離することで、コードの可読性・再利用性・拡張性を向上させることができます。

# 2. 事前準備

QiitaAPIで記事の新規投稿を行うため、`write_qiita`のtokenを取得する必要があります。
私は以下の記事を参照してtokenを取得しました。

[Qiita APIを触ってみる](https://qiita.com/miyuki_samitani/items/bfe89abb039a07e652e6 "Qiita APIを触ってみる")

# 3. APIエンドポイントに接続するメソッドを手続き型で書いてみる
まずQiitaAPIを利用した記事の新規投稿メソッドを手続き型で書いてみました。
```ruby:index.rb
require 'json'
require 'net/http'

BASE_URL = 'https://qiita.com'
MY_TOKEN = "QiitaAPIのtoken文字列"

# 記事の新規投稿
def create(title:, body:, tag:, is_private: true)
  # Httpsリクエストの作成と設定
  uri = URI(BASE_URL)
  https = Net::HTTP.new(uri.hostname, uri.port)
  https.use_ssl = true
  https.verify_mode = OpenSSL::SSL::VERIFY_NONE
  request = Net::HTTP::Post.new('/api/v2/items')
  request["Authorization"] = "Bearer #{MY_TOKEN}"
  request["Content-Type"] = 'application/json'
  
  # 記事タイトル・本文・公開設定・タグをリクエストボディに追加
  request.body = {
    title: title,
    body: body,
    private: is_private,
    tags: [{ name: tag }]
  }.to_json
  
  # POSTリクエストを送信し、結果を取得
  response = https.request(request)

  # POSTリクエストが成功したかどうかで分岐
  if response.kind_of?(Net::HTTPSuccess)
    response_body = JSON.parse(response.body)
    { is_error: false,
      article_id: response_body['id'],
      title: response_body['title'],
      url: response_body['url'],
      body: response_body['body'],
      tags: response_body['tags']
    }
  else
    { is_error: true }
  end
end

new_article = create(title: 'hoge', body: 'hoge', tag: 'hoge')
# 新規投稿に成功した場合
# => {:is_error=>false,
# =>  :article_id=>"#{記事ID}",
# =>  :title=>"hoge",
# =>  :url=>"https://qiita.com/#{ユーザー名}/private/#{記事ID}",
# =>  :body=>"hoge\n",
# =>  :tags=>[{"name"=>"hoge", "versions"=>[]}]}

# 新規投稿に失敗した場合
# => {:is_error=>true}
```
上記のコードを読んで、皆さんはどのように感じましたか？
`create`メソッドが長すぎるな〜と感じた方も多いと思います。
もしくは、APIエンドポイントに接続する部分が再利用しにくそうだな〜と感じた方もいるかもしれません。

上記のように感じる原因は`create`メソッドの役割が多すぎるからです。
以下の役割をこの`create`メソッドたった一つで担当しています。

- HTTPS通信を行うための設定
- POSTリクエストを行うための設定
- POSTリクエストを送信して結果を取得
- 成功したかどうかの分岐判断
- 返却値を生成


もちろん、以下のような場合は上記コードでも良いのかもしれません。

:::note warn
他ファイル・他メソッドでは、QiitaAPIの新規投稿エンドポイントは使用しない
:::
:::note warn
他のQiitaAPIエンドポイントは使用しない
:::

でも実際には、再利用したかったり他のエンドポイントも使用したかったりする場合が多いですよね。
その度に今回と同じように手続き型でロジックを書いていくと、際限なくコードが肥大化していく可能性があります。

なので、可能な限りAPIエンドポイントに接続して結果を取得するようなメソッドは、以下のような条件を満たしている方が良いですよね。

:::note info
アプリケーションのどのファイル、メソッドからでも実行できる
:::

:::note info
APIエンドポイントの接続メソッドを増やす場合に拡張しやすい
:::

そこで今回は、上記のコードをリファクタしながら再利用しやすい・拡張しやすいコードに修正していきます。

# 4. QiitaAPIのエンドポイントをモジュール化しよう
再利用しやすい・拡張しやすいコードにするために、APIのエンドポイントに接続する部分をAPIクライアントとしてモジュール化していきます。
大まかな役割分担は以下の通りです。

①APIクライアントモジュール
- API通信を実際に行う
- HTTPS通信が成功したかどうかの検証を行う
- 検証結果をもとに返却値を生成して、呼び出し元メソッドへ生成した返却値を渡す

②index.rbの`create`メソッド
- モジュール化したAPIクライアントを呼び出す
- ApiClientから返却値を受け取る



## 4-1. APIクライアントモジュールを作る
では実際にAPIクライアントモジュールを作成していきましょう。
以下2つのファイルを作成します。
```ruby:api_client.rb
require 'net/http'
require 'json'

module Qiita
  module ApiClient
    class Client
      BASE_URL = 'https://qiita.com'
      API_TOKEN = 'QiitaAPIのtoken文字列'

      def initialize
      end
    end
  end
end
```
```ruby:create_article_response.rb
module Qiita
  module ApiClient
    class CreateArticleResponse
      def initialize
      end
    end
  end
end
```

2つのファイルでは、`Qiita::ApiClient`モジュール内に`Client`クラスと`CreateArticleResponse`クラスが定義されています。

この2つのクラスの役割は以下の通りです。

①`Qiita::ApiClient::Client`クラスの役割
- HTTPS通信の設定を行う
- APIのエンドポイントに接続し、結果を受け取る
- 結果を`Qiita::ApiClient::CreateArticleResponse`クラスに渡し、返却値を生成してもらう
- 生成してもらった返却値を呼び出し元メソッドに返す

②`Qiita::ApiClient::CreateArticleResponse`クラスの役割
- `Qiita::ApiClient::Client`クラスから結果を受け取る
- 結果をもとに、HTTPS通信が成功したかどうかの検証を行う
- 検証結果をもとに返却値を生成し、`Qiita::ApiClient::Client`クラスに渡す

## 4-2. Qiita::ApiClient::Clientクラスを実装する
`Qiita::ApiClient::Client`クラスの役割は以下の通りでした。
- HTTPS通信の設定を行う
- APIのエンドポイントに接続し、結果を受け取る
- 結果を`Qiita::ApiClient::CreateArticleResponse`クラスに渡し、返却値を生成してもらう
- 生成してもらった返却値を、呼び出し元メソッドに返す

では、上記役割を一つずつ実装していきましょう。

### 4-2-1. HTTPS通信の設定メソッドを実装する

HTTPS通信を行うための設定メソッドを実装します。

```ruby:api_client.rb
require 'net/http'
require 'json'

module Qiita
  module ApiClient
    class Client
      BASE_URL = 'https://qiita.com'
      API_TOKEN = 'QiitaAPIのtoken文字列'

      def initialize
      end

      private

      def create_https_request
        uri = URI(BASE_URL)
        https = Net::HTTP.new(uri.host_name, uri.port)
        https.use_ssl = true
        https.verify_mode = OpenSSL::SSL::VERIFY_NONE
        https
      end
    end
  end
end
```

### 4-2-2. HTTPS通信の設定オプションを外部から受け取れるようにする

HTTPS通信の設定項目については、モジュールを呼び出す側からオプションで変更できるようにしたいです。

そのため`initialize`メソッドの引数で受け取れるようにコードを修正します。

また、引数には初期値を持たせることで、引数なしでモジュールを呼び出したときにも正常に動作するように設計します。


```ruby:api_client.rb
require 'net/http'
require 'json'

module Qiita
  module ApiClient
    class Client
      BASE_URL = 'https://qiita.com'
      API_TOKEN = 'QiitaAPIのtoken文字列'

      def initialize(port: nil, is_use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE)
        uri = URI(BASE_URL)
        @host_name = uri.hostname
        @port = port || uri.port
        @is_use_ssl = is_use_ssl
        @verify_mode = verify_mode
      end

      private

      def create_https_request
        https = Net::HTTP.new(@host_name, @port)
        https.use_ssl = @is_use_ssl
        https.verify_mode = @verify_mode
        https
      end
    end
  end
end
```

### 4-2-3. QiitaAPIのエンドポイントに接続し、結果を受け取るメソッドを実装する
次に、実際にQiitaAPIのエンドポイントに接続するためのメソッドを定義します。

今回は引数として、記事タイトル・記事本文・タグ・公開設定を受け取れるようにします。

```ruby:api_client.rb
require 'net/http'
require 'json'

module Qiita
  module ApiClient
    class Client
      BASE_URL = 'https://qiita.com'
      API_TOKEN = 'QiitaAPIのtoken文字列'

      def initialize(port: nil, is_use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE)
        uri = URI(BASE_URL)
        @host_name = uri.hostname
        @port = port || uri.port
        @is_use_ssl = is_use_ssl
        @verify_mode = verify_mode
      end

      def create_article(title:, body:, tag:, is_private: true)
        https = create_https_request
        request = Net::HTTP::Post.new('/api/v2/items')
        request["Authorization"] = "Bearer #{API_TOKEN}"
        request["Content-Type"] = 'application/json'
        request.body = {
          title: title,
          body: body,
          private: is_private,
          tags: [{ name: tag }]
        }.to_json
        response = https.request(request)

        # => #<Net::HTTPCreated 201 Created readbody=true>
      end

      private

      def create_https_request
        https = Net::HTTP.new(@host_name, @port)
        https.use_ssl = @is_use_ssl
        https.verify_mode = @verify_mode
        https
      end
    end
  end
end
```
### 4-2-4. Qiita::ApiClient::CreateArticleResponseクラスに通信結果を渡す
最後に、取得した結果を`Qiita::ApiClient::CreateArticleResponse`に渡します。

```ruby:api_client.rb
require 'net/http'
require 'json'
require './create_article_response' # 追加

module Qiita
  module ApiClient
    class Client
      # 省略
      def create_article(title:, body:, tag:, is_private: true)
        https = create_https_request
        request = Net::HTTP::Post.new('/api/v2/items')
        request["Authorization"] = "Bearer #{API_TOKEN}"
        request["Content-Type"] = 'application/json'
        request.body = {
          title: title,
          body: body,
          private: is_private,
          tags: [{ name: tag }]
        }.to_json
        response = https.request(request)
        Qiita::ApiClient::CreateArticleResponse.new(response) # 追加
      end

      private
      # 省略
    end
  end
end
```




これでQiitaAPIのエンドポイントに接続する部分をモジュールに切り出すことができました。

次に、`Qiita::ApiClient::CreateArticleResponse`クラスを実装していきます。


## 4-3. Qiita::ApiClient::CreateArticleResponseクラスを実装する
`Qiita::ApiClient::CreateArticleResponse`クラスの役割は以下の通りでした。
- `Qiita::ApiClient::Client`クラスから結果を受け取る
- 結果をもとに、HTTPS通信が成功したかどうかの検証を行う
- 検証結果をもとに返却値を生成し、`Qiita::ApiClient::Client`クラスに渡す


### 4-3-1. インスタンス変数の読み取りを可能にする
では`Qiita::ApiClient::CreateArticleResponse`クラスを実装していきます。

まず、`attr_reader`を定義してインスタンス変数の読み取りを可能にします。

今回は
- ステータスコード
- エラー有無
- エラーメッセージ
- 記事ID
- 記事タイトル
- URL
- 記事本文
- タグ

を読み取り可能なインスタンス変数として設定します。

```ruby:create_article_response.rb
module Qiita
  module ApiClient
    class CreateArticleResponse
      attr_reader :response_code,
                  :is_error,
                  :error_message,
                  :post_id,
                  :title,
                  :url,
                  :body,
                  :tags

      def initialize
      end
    end
  end
end
```

### 4-3-2. Qiita::ApiClient::Clientクラスから通信結果を受け取る
次に、`Qiita::ApiClient::Client`クラスから通信結果を受け取るために`initialize`を定義します。

通信結果を表すステータスコードも`@response_code`に格納しておきます。

```ruby:create_article_response.rb
module Qiita
  module ApiClient
    class CreateArticleResponse
      attr_reader :response_code,
                  :is_error,
                  :error_message,
                  :post_id,
                  :title,
                  :url,
                  :body,
                  :tags

      def initialize(response)
        @response_code = response.code
      end
    end
  end
end
```

### 4-3-3. 結果をもとに、HTTPS通信が成功したかどうかの検証を行う
`@response_code`の値に応じて`@is_error`にboolean値を入れます。
今回は、HTTPステータスコードが200番台であれば`false`を、それ以外の場合は`true`を入れることにします。

```ruby:create_article_response.rb
module Qiita
  module ApiClient
    class CreateArticleResponse
      attr_reader :response_code,
                  :is_error,
                  :error_message,
                  :post_id,
                  :title,
                  :url,
                  :body,
                  :tags

      def initialize(response)
        @response_code = response.code
        @is_error = !response.kind_of?(Net::HTTPSuccess)
      end
    end
  end
end
```

### 4-3-4. 検証結果をもとに返却値を生成し、Qiita::ApiClient::Clientクラスに渡す

最後に、検証結果をもとに返却値を生成します。

まず、`@is_error`が`true`の場合は`@error_message`に値を格納し処理を終了させます。

```ruby:create_article_response.rb
module Qiita
  module ApiClient
    class CreateArticleResponse
      attr_reader :response_code,
                  :is_error,
                  :error_message,
                  :post_id,
                  :title,
                  :url,
                  :body,
                  :tags

      def initialize(response)
        @response_code = response.code
        @is_error = !response.kind_of?(Net::HTTPSuccess)
        @error_message = response.message if is_error
        return if is_error
      end
    end
  end
end
```

`@is_error`が`false`の場合は、その他のインスタンス変数に値を格納していきます。

このように、通信結果を判断するインスタンス変数を作成することで、通信結果に応じて返却値を変更できます。


```ruby:create_article_response.rb
module Qiita
  module ApiClient
    class CreateArticleResponse
      attr_reader :response_code,
                  :is_error,
                  :error_message,
                  :post_id,
                  :title,
                  :url,
                  :body,
                  :tags

      def initialize(response)
        @response_code = response.code
        @is_error = !response.kind_of?(Net::HTTPSuccess)
        @error_message = response.message if @is_error
        return if is_error
        
        response_body = JSON.parse(response.body)
        @post_id = response_body['id']
        @title = response_body['title']
        @url = response_body['url']
        @body = response_body['body']
        @tags = response_body['tags']
      end
    end
  end
end
```

これで`Qiita::ApiClient::CreateArticleResponse`クラスの実装は完了です。

## 4-4. Qiita::ApiClientモジュールの全体像
以下が実装したコードの全体像です。

### 4-4-1. Qiita::ApiClient::Clientクラスの全体像
```ruby:api_client.rb
require 'net/http'
require 'json'
require './create_article_response'

module Qiita
  module ApiClient
    class Client
      BASE_URL = 'https://qiita.com'
      API_TOKEN = 'QiitaAPIのtoken文字列'

      def initialize(port: nil, is_use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE)
        uri = URI(BASE_URL)
        @host_name = uri.hostname
        @port = port || uri.port
        @is_use_ssl = is_use_ssl
        @verify_mode = verify_mode
      end

      def create_article(title:, body:, tag:, is_private: true)
        https = create_https_request
        request = Net::HTTP::Post.new('/api/v2/items')
        request["Authorization"] = "Bearer #{API_TOKEN}"
        request["Content-Type"] = 'application/json'
        request.body = {
          title: title,
          body: body,
          private: is_private,
          tags: [{ name: tag }]
        }.to_json
        response = https.request(request)
        Qiita::ApiClient::CreateArticleResponse.new(response)
      end

      private

      def create_https_request
        https = Net::HTTP.new(@host_name, @port)
        https.use_ssl = @is_use_ssl
        https.verify_mode = @verify_mode
        https
      end
    end
  end
end
```
### 4-4-2. Qiita::ApiClient::CreateArticleResponseクラスの全体像
```ruby:create_article_response.rb
module Qiita
  module ApiClient
    class CreateArticleResponse
      attr_reader :response_code,
                  :is_error,
                  :error_message,
                  :post_id,
                  :title,
                  :url,
                  :body,
                  :tags

      def initialize(response)
        @response_code = response.code
        @is_error = !response.kind_of?(Net::HTTPSuccess)
        @error_message = response.message if @is_error
        return if is_error
        
        response_body = JSON.parse(response.body)
        @post_id = response_body['id']
        @title = response_body['title']
        @url = response_body['url']
        @body = response_body['body']
        @tags = response_body['tags']
      end
    end
  end
end
```

## 4-5. index.rbのcreateメソッドを修正する
`Qiita::ApiClient`モジュールの実装が完了したので、最後にindex.rbの修正を行います。

index.rbの`create`メソッドの役割は以下の通りでした。

- モジュール化したAPIクライアントを呼び出す
- APIクライアントから返却値を受け取る

QiitaAPIのエンドポイントをモジュール化したことで、`create`メソッドはモジュールを呼び出し結果を受け取る役割だけに集中することができるようになりました。

```ruby:index.rb
require './api_client'

# 記事の新規投稿
def create(title:, body:, tag:, is_private: true)
  # 記事の新規投稿を行い、結果を取得
  Qiita::ApiClient::Client.new.create_article(title: title,
                                              body: body,
                                              tag: tag,
                                              is_private: is_private
  )
end

new_article = create(title: 'hoge', body: 'hoge', tag: 'hoge')
# 新規投稿に成功した場合
# => <Qiita::ApiClient::CreateArticleResponse:0x00007f8489823e2
# =>  @body="hoge\n",
# =>  @is_error=false,
# =>  @article_id="#{記事ID}",
# =>  @response_code=201,
# =>  @tags=[{"name"=>"hoge", "versions"=>[]}],
# =>  @title="hoge",
# =>  @url="https://qiita.com/#{ユーザー名}/private/#{記事ID}">

# 新規投稿に失敗した場合（404エラーの場合）
# => <Qiita::ApiClient::CreateArticleResponse:0x00007f8cda014a28
# =>  @error_message="Not Found",
# =>  @is_error=true,
# =>  @response_code=404>
```
# 5. まとめ
QiitaAPIのエンドポイントをモジュール化することで以下の通り役割を分担できました。

①index.rbの`create`メソッドの役割
- モジュール化したAPIクライアントを呼び出す
- APIクライアントから返却値を受け取る

②`Qiita::ApiClient::Client`クラスの役割
- HTTPS通信の設定を行う
- APIのエンドポイントに接続し、結果を受け取る
- 結果を`Qiita::ApiClient::CreateArticleResponse`クラスに渡し、返却値を生成してもらう
- 生成してもらった返却値を呼び出し元メソッドに返す

③`Qiita::ApiClient::CreateArticleResponse`クラスの役割
- `Qiita::ApiClient::Client`クラスから結果を受け取る
- 結果をもとに、HTTPS通信が成功したかどうかの検証を行う
- 検証結果をもとに返却値を生成し、`Qiita::ApiClient::Client`クラスに渡す

役割を分担したことで、以下の効果が生まれました。

:::note info
createメソッドは、アプリケーション自体のロジックのみ記載できるようになる
:::

:::note info
別ファイルや別メソッドからもQiitaAPIのエンドポイントに接続できるようになる
:::

:::note info
モジュールにメソッドやクラスを追加することで、QiitaAPIの拡張がしやすくなる
:::

役割を分担することで使い回しがしやすいコードになリます。

ぜひ皆さんも試してみてくださいね！