アプリケーション開発をするにあたり、外部サービスと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.newでHTTPのクライアントのオブジェクトを作成
- Net::HTTP::Get.newでGETリクエストのオブジェクトを作成
- 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)やってます。フォローしてもらえるとうれしいです!