【Ruby】net/httpを利用した外部API連携クラスの作成手順

Ruby

アプリケーション開発をするにあたり、外部サービスとAPI連携を行う場合があります。

RubyではHTTP通信を行う標準ライブラリとしてnet/httpが用意されています。
今回はnet/httpを利用して、API経由で外部サービスのデータを取得する方法について紹介します。

今回作成するもの

今回は外部サービスの例としてQiitaを利用します。

今回のゴール
  • QiitaClientAPIというQiita APIと連携するクラスを作成する
  • get_itemsというクラスメソッドを利用することでQiitaの記事一覧が取得できるようにする
  • API連携でエラーが発生した場合は、エラーレスポンスの内容を例外として出力する
  • localhost:3000/qiita_itemsにアクセスするとQiitaClientAPI.get_itemsの結果が取得できる

なお、今回紹介するサンプルはrails 6.0.3.4のAPIモードで作成しています。

最終的な成果物

今回は手順を追いながらソースコードの紹介をしていきます。
最終的なアウトプットは以下のようになります。

ソースコード

config/routes.rb

Rails.application.routes.draw do
  resources :qiita_items, only: %i(index)
end

app/controllers/qiita_items_controller.rb

class QiitaItemsController < ApplicationController
  def index
    response_json = QiitaApiClient.get_items

    # レスポンスを簡略化するため、titleプロパティのみ返すようにしている
    render json: response_json.map {|item| item.slice('title') }
  end
end

lib/qiita_api_client.rb

class QiitaApiClient
  class HTTPError < StandardError
    def initialize(response)
      super "code=#{response.code} body=#{response.body}"
    end
  end

  def initialize
    @token = Rails.application.credentials.qiita[:token]
  end

  def get_items
    request = Net::HTTP::Get.new(
      '/api/v2/items',
      'Authorization' => "Bearer #{@token}"
    )
    http_client.request(request)
    response = http_client.request(request)
    case response
    when Net::HTTPSuccess
      JSON.parse(response.body)
    else
      raise QiitaApiClient::HTTPError.new(response)
    end
  end

  class << self
    def client
      QiitaApiClient.new
    end

    def get_items
      client.get_items
    end
  end

  private

  QIITA_HOST = 'https://qiita.com'

  def http_client
    uri = URI.parse(QIITA_HOST)
    http_client = Net::HTTP.new(uri.host, uri.port)
    http_client.use_ssl = true
    http_client
  end
end

実行結果

API連携が成功した場合は以下のような結果になります。

$ curl 'http://localhost:3000/qiita_items'

[{"title":"highlight.jsを動的に使ってみた - CodePen"},{"title":"飛び飛びセル順次コピペ"},{"title":"テキスト入力中の点滅するカーソルに好きなCSSを当てる方法"},{"title":"jQueryいろいろ(wrapAll, MutationObserverなど)"},{"title":"高機能なSQL開発ツール「A5:SQL Mk-2」をUbuntuで使う"},{"title":"Amazon Aurora カスタムエンドポイントの検証と考察"},...
(略)
...
]

API連携が失敗した場合は以下のような結果になります。

### 不正なトークンが利用されている場合
$ curl 'http://localhost:3000/qiita_items'

QiitaApiClient::HTTPError (code=401 body={"message":"Unauthorized","type":"unauthorized"})

下準備

実装をするにあたり、Qiita APIと連携するための準備をします。

アクセストークンの取得

Qiita APIの認証認可に必要なアクセストークンを取得します。

アクセストークンはユーザの管理画面で取得できます。

なおQiita APIのGETリクエストではアクセストークンは不要なため1、今回紹介する記事一覧取得API(/api/v2/items)のみを実装したい場合はこの作業は不要です。

アクセストークンをRailsアプリケーションに登録する

今回はRails 6のcredentialsにトークンを保存しました。

### config/credentials/development.yml.encの編集
$ export EDITOR="vim"
$ rails credentials:edit -e development
→ このタイミングでconfig/credentials/development.keyとconfig/credentials/development.yml.encが作成される

config/credentials/development.yml.enc

qiita:
  # 取得したトークンをセットする
  token: xxxxxxx
$ rails c

### 取得したトークンが表示されればOK
> Rails.application.credentials.qiita[:token]
=> 'xxxxxxx'

ルーティングの追加

検証で利用するlocalhost:3000/qiita_itemsのエンドポイントを作成します。

config/routes.rb

Rails.application.routes.draw do
  resources :qiita_items, only: %i(index)
end

lib配下のクラスを読み込むようにする

今回はQiita APIと連携するクラスをlib配下に作成します。
lib配下のクラスが読み込まれるようにするため以下のように修正します。

config/application.rb

module RailsApiClient
  class Application < Rails::Application
    # 以下を追加
    config.paths.add 'lib', eager_load: true
  end
end

net/httpを利用した外部APIとの連携方法

ここからは順を追って実装について紹介していきます。

シンプルな方法

Qiita APIと連携するクラスを作成せず、ロジック2を直接記述するパターンです。

app/controllers/qiita_items_controller.rb

class QiitaItemsController < ApplicationController
  def index
    uri = URI.parse('https://qiita.com')
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    request = Net::HTTP::Get.new(
      '/api/v2/items',
      'Authorization' => "Bearer #{Rails.application.credentials.qiita[:token]}"
    )
    response = http.request(request)
    response_json = JSON.parse(response.body)

    # レスポンスを簡略化するため、titleプロパティのみ返すようにしている
    render json: response_json.map {|item| item.slice('title') }
  end
end

ソースコードを見ればロジックはわかると思いますが、net/httpを利用したGETリクエストの流れについて改めてまとめると以下のようになります。

net/httpを利用したGETリクエストの流れ
  1. Net::HTTP.newでHTTPのクライアントのオブジェクトを作成
  2. Net::HTTP::Get.newでGETリクエストのオブジェクトを作成
  3. HTTPクライアントを利用してGETのリクエスト

Qiita APIと連携する専用クラスを作成する

QiitaApiClient.get_itemsを呼ぶことでデータが取得できるようにします。
lib配下にQiitaApiClientクラスを作成し、API連携のロジックを移行します。

lib/qiita_api_client.rb

class QiitaApiClient
  class << self

    QIITA_HOST = 'https://qiita.com'

    def get_items
      uri = URI.parse(QIITA_HOST)
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl = true # SSLを有効化する
      request = Net::HTTP::Get.new(
        '/api/v2/items', # 記事一覧を取得するエンドポイント
        'Authorization' => "Bearer #{Rails.application.credentials.qiita[:token]}" # Bearer認証
      )
      response = http.request(request)
      JSON.parse(response.body)
    end
  end
end

API連携のロジックをQiitaApiClientに移行したので呼び出す側は以下のようになります。

app/controllers/qiita_items_controller.rb

class QiitaItemsController < ApplicationController
  def index
    response_json = QiitaApiClient.get_items
    render json: response_json.map {|item| item.slice('title') }
  end
end

リファクタ: Net::HTTPオブジェクトの作成を共通化する

このままでも問題ないのですが、クラスメソッド(今回でいうget_items)でNet::HTTP.newを実行しているため、クラスメソッドを追加するたびにNet::HTTP.newも追加されてソースコードが少し冗長になります。

そこで、Net::HTTP.newを実行するインスタンスメソッドを作成します。

lib/qiita_api_client.rb

class QiitaApiClient
  def initialize
    # トークンはインスタンス変数として呼び出せるようにする
    @token = Rails.application.credentials.qiita[:token]
  end

  def get_items
    request = Net::HTTP::Get.new(
      '/api/v2/items',
      'Authorization' => "Bearer #{@token}"
    )
    # self.http_clientを呼び出す
    response = http_client.request(request)
    JSON.parse(response.body)
  end

  private

  QIITA_HOST = 'https://qiita.com'

  def http_client
    uri = URI.parse(QIITA_HOST)
    http_client = Net::HTTP.new(uri.host, uri.port)
    http_client.use_ssl = true
    http_client
  end
end

QiitaApiClientを呼び出す側は以下のようになります。

app/controllers/qiita_items_controller.rb

class QiitaItemsController < ApplicationController
  def index
    qiita_client = QiitaApiClient.new
    response_json = qiita_client.get_items
    render json: response_json.map {|item| item.slice('title') }
  end
end

リファクタ: QiitaApiClientインスタンスを呼び出し側で作成しなくて済むようにする

インスタンスを作成しなくてもQiitaApiClient.get_itemsを実行するだけでAPI連携できるようにリファクタリングした結果は以下のとおりです。

lib/qiita_api_client.rb

class QiitaApiClient
  def initialize
    @token = Rails.application.credentials.qiita[:token]
  end

  def get_items
    request = Net::HTTP::Get.new(
      '/api/v2/items',
      'Authorization' => "Bearer #{@token}"
    )
    response = http_client.request(request)
    JSON.parse(response.body)
  end

  class << self
    def client
      QiitaApiClient.new
    end

    def get_items
      client.get_items
    end
  end

  private

  QIITA_HOST = 'https://qiita.com'

  def http_client
    uri = URI.parse(QIITA_HOST)
    http_client = Net::HTTP.new(uri.host, uri.port)
    http_client.use_ssl = true
    http_client
  end
end

これで、以下のコードでQiita APIからデータを取得できるようになりました。

app/controllers/qiita_items_controller.rb

class QiitaItemsController < ApplicationController
  def index
    response_json = QiitaApiClient.get_items
    render json: response_json.map {|item| item.slice('title') }
  end
end

例外処理の追加

Qiita APIのレスポンスがエラーだった場合、APIのエラーの内容がわかるよう例外処理を追加します。
今回はエラー時のレスポンスの内容とエラーコードを例外のメッセージに追加しました。

lib/qiita_api_client.rb

class QiitaApiClient
  def initialize
    @token = Rails.application.credentials.qiita[:token]
  end

  def get_items
    request = Net::HTTP::Get.new(
      '/api/v2/items',
      'Authorization' => "Bearer #{@token}"
    )
    response = http_client.request(request)
    case response
    when Net::HTTPSuccess
      JSON.parse(response.body)
    else
      raise "code= #{response.code}, body = #{response.body}"
    end
  end

  class << self
    def client
      QiitaApiClient.new
    end

    def get_items
      client.get_items
    end
  end

  private

  QIITA_HOST = 'https://qiita.com'

  def http_client
    uri = URI.parse(QIITA_HOST)
    http_client = Net::HTTP.new(uri.host, uri.port)
    http_client.use_ssl = true
    http_client
  end
end

たとえば、不正なトークンをセットしてリクエストを送った場合、以下のような例外が発生します。

$ curl 'http://localhost:3000/qiita_items'

RuntimeError (code= 401, body = {"message":"Unauthorized","type":"unauthorized"}):

カスタム例外を作成する

カスタム例外には例外の発生場所がわかりやすくなるというメリットがあります。カスタム例外の作成については賛否両論ありますが一応紹介しておきます。

今回はStandardErrorを継承したHTTPErrorというカスタム例外を作成しました。

lib/qiita_api_client.rb

class QiitaApiClient
  class HTTPError < StandardError
    def initialize(response)
      super "code=#{response.code} body=#{response.body}"
    end
  end

  def initialize
    @token = Rails.application.credentials.qiita[:token]
  end

  def get_items
    request = Net::HTTP::Get.new(
      '/api/v2/items',
      'Authorization' => "Bearer #{@token}"
    )
    http_client.request(request)
    response = http_client.request(request)
    case response
    when Net::HTTPSuccess
      JSON.parse(response.body)
    else
      raise QiitaApiClient::HTTPError.new(response)
    end
  end

  class << self
    def client
      QiitaApiClient.new
    end

    def get_items
      client.get_items
    end
  end

  private

  QIITA_HOST = 'https://qiita.com'

  def http_client
    uri = URI.parse(QIITA_HOST)
    http_client = Net::HTTP.new(uri.host, uri.port)
    http_client.use_ssl = true
    http_client
  end
end

これにより、カスタム例外のクラスで例外処理がされるようになりました。

$ curl 'http://localhost:3000/qiita_items'

QiitaApiClient::HTTPError (code=401 body={"message":"Unauthorized","type":"unauthorized"}):

まとめ

以上でnet/httpを利用した外部API連携の方法の紹介を終わります。

外部APIと連携する専用のクラスを作成することでQiitaApiClient.get_itemsのような形で外部サービスのデータを取得できます。
今回はシンプルなGETメソッドのみを実装しましたが、同様の手順でクエリやリクエストボディのついたメソッドの実装もできます。

今回のサンプルはあくまで一例ですので、よりよい方法があれば教えていただけるとありがたいです。

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!