はじめに
「新人プログラマ応援」としていますが、私が新しいAPIを書く際に「あれ?これってどこらへん見れば書いてあるっけ?」というのをまとめておきたかったというのが主な目的です。一応、プログラミング言語やフレームワークに依存しないWebに関する知識を一通り得られるようにしていますのでよろしければお読みください。
なお、「フレームワークに依存しない」とは言うものの実装例を示したいものもあるので実装例にはFlaskを使います。
各論に入る前に
Web APIというかAPIについて考えてみましょう。APIには次の3つが必要です。
- APIの名前
- APIの引数
- APIの結果
Web APIの場合、これらが次のように対応します。
- APIの名前:パス
- APIの引数:クエリストリングやらボディーで渡すJSONやら
- APIの結果:ステータスコードとボディー
では各論に入りましょう。
パス
APIの名前に相当するのがパスです。まだHTTPの話はしていませんがHTTPでAPIにアクセスする際には以下のように指定します。
GET /messages HTTP/1.1
Flaskでこのリクエストに応答する処理を書くと以下のようになります。
@app.route('/messages', methods=['GET'])
def get_messages():
# 何処かからmessagesを取得
return jsonify(messages)
個人的にはこのようなリクエストと処理関数が一対一に書けるマイクロフレームワークが最近のお気に入りです。
パスパラメータ
引数についてはもう少し後で説明しますがパスに関連してパスパラメータの説明をします。パスパラメータとは/messages/1
のように指定された1
を引数として受け取る方法です。例えばFlaskでは以下のようになります。
@app.route('/messages/<id>', methods=['GET'])
def get_message(id):
# idに対応するmessageを取得
return jsonify(message)
パスパラメータの指定方法はフレームワークにより異なっています。Railsは/messages/:id
ですし、Springは/messages/{id}
のようです。DjangoはFlaskと同じ、SinatraはRailsと同じなので同一言語の場合はどちらかがどちらかに寄せてる感じなのかな?
APIを設計する際はパスパラメータも含めて「きれいに」パスを設計するべきと言われています。初めのうちはともかく機能追加していくと徐々に汚いパスになってしまうのが悩みどころです。
HTTP
さてそれでは引数、とは残念ながらなりません。引数の前にHTTPについて順番に理解していく必要があります。
メソッド
HTTPでサーバにリクエストを送るときに指定されるのがメソッドです。動詞(verb)とも呼ばれるようです。情報を取得するときはGET
で、情報を送信するときはPOST
です。GETとPOST以外にもメソッドが定義されており、APIを作るときには「適切なメソッドで」呼ばれるようにすべきです。一般的には以下のようになります。
- GET:取得
- POST:作成
- PUT:更新
- DELETE:削除
Flaskの例だとこのようになります。
@app.route('/messages/<id>', methods=['GET'])
def get_message(id):
# idに対応するmessageを取得
@app.route('/messages', methods=['POST'])
def create_message():
# 渡されたデータから新しいmessageを作成
# ※メッセージ本体は「関数引数」とは別に取得
@app.route('/messages/<id>', methods=['PUT'])
def update_message(id):
# idに対応するmessageを更新
@app.route('/messages/<id>', methods=['DELETE'])
def delete_messages(id):
# idに対応するmessageを削除
ステータスコード
引数の前に結果の話です。HTTPのリクエストを送ると三桁のステータスコードが返されます。大きく分けると200系が正常、400系が異常(エラー)です。
ステータスコードは一覧を見て考えます。
正常終了の場合は200
、APIにより201
や204
を返すこともあります(まあ呼ぶ側も自分で書いているので細かく見てないですけど・・・)
「渡された引数がおかしい」場合は400
、「指定された内容がない」場合は404
を返します。認証・認可が必要なAPIを作る場合は401
や403
を返すこともあります。認証については後で説明します。
なお400系のステータスコードは正確には「クライアントエラー(クライアントの指定が何かおかしい)」でこれとは別に500系のサーバエラーがあります。ただし500系のステータスコードを自分で返すことはまずなく、APIやWebアプリケーションでこのステータスコードが返された場合はバグ(自分の書いたコードで例外が起きてフレームワークがキャッチし500
を返した)ということになります。
もう一つステータスコードについて。「APIを呼び出す側」が400系もしくは500系のステータスコードを受け取ったときにどのような動作をするかも注意が必要です。エラーのステータスコードを受けてもそのまま返すライブラリもあれば例外を投げるライブラリもあります。
ヘッダ
リクエストやレスポンスには以下の形式でヘッダ(header)を付けることができます。
Name: Value
ヘッダも様々なものが定義されているのですが、APIでの使用という点ではContent-Type
、後述するCORSや認証のためのヘッダぐらいをまずは覚えればいいと思います。
クエリストリング
さてようやく引数にたどり着きました。まずはクエリストリングです。クエリストリングというのは、/search?q=HTTP
のようなパスのq=HTTP
の部分です。複数の引数を指定する場合は&で区切って、/search?q=HTTP&date_from=20200901&date_to=20200930
みたいにします。普通のフレームワークであれば自力で解析処理を書く必要はなく個々の引数を取得するための方法が提供されているでしょう。Flaskの場合は以下のように書きます。
from flask import request
@app.route('/search', methods=['GET'])
def search():
query = request.args.get('q', '')
JSON
構造化されたデータをやり取りする(サーバに送る、サーバから受け取る)場合にはJSONを使います。配列やオブジェクト(連想配列)を使うことができます。
{
"id": "2f7c42b2a88dc08d533e",
"title": "Web APIを作るために必要な知識をまとめてみる",
"tags": ["WebAPI", "新人プログラマ応援", "HTTP"]
}
データを送る際にはHTTPヘッダでContent-Type: application/json
と指定します。普通のフレームワークであればこのContent-Typeが指定されていれば自動的に言語が提供するデータ構造に変換してくれるでしょう。例えばFlaskであれば以下のようになります。
@app.route('/messages', methods=['POST']
def create_message():
message = request.get_json()
また、結果をJSONで返す方法も提供されているでしょう。例によってFlaskでの書き方です。
from flask import jsonify
@app.route('messages', methods=['GET'])
def get_messages():
return jsonify(messages)
CORS
ぼくがかんがえたさいこうのえーぴーあいをブラウザJSから呼び出してみるとよく引っかかるのがこれです。というかこのまとめを作ろうとしたそもそもの動機がこのCORS(を含めたWeb APIを作るときに気を付けるべきまとまった確認項目がほしいという理由)です。
セキュリティ上の理由から「JSを取得したドメインと別ドメインにJSでリクエストを送る(非同期通信をする)」とブラウザによりはねられます。理由はもちろんわかりますよね。
この制限を緩めるのがCORSです。CORSは以下のように動作します。1
- ブラウザJSがリクエストを送ろうとしたら
- 本体の送信前にブラウザがサーバに対してOPTIONSメソッドを送る(プリフライトリクエスト)
- サーバが返してきたHTTPヘッダを調べて「送ろうとしているリクエスト」が許可されているかチェック
- 許可されてなかったらエラー終了
- 許可されてたら本体のリクエストを送る
OPTIONSメソッドのレスポンスとして使われるのが次の3つのHTTPヘッダです。他にもありますがまずはこの3つを覚えればいいでしょう。
- Access-Control-Allow-Origin
- どのドメインからのリクエストは許可するか。
*
で全ドメイン - Access-Control-Allow-Methods
- どのメソッドは許可するか
- Access-Control-Allow-Headers
- どのHTTPヘッダは許可するか
なるほど、というわけでこの仕様(OPTIONSメソッド)を実装するわけですがまあめんどくさいです。ここで手抜きをするには例によってFlaskだと以下のようになります。
from flask_cors import CORS
CORS(app)
超簡単
と言いたいところですがデフォルトの設定はかなりざるです。このようなセキュリティチェックの機能がある意味をよく考え適切な設定を行うようにしましょう。
認証
最後に伏線しておいた認証です。というか認証自体の処理はおいといて「認証されていること」をどうサーバに渡すかの話です。HTTPヘッダのAuthorization
を使います。AuthorizationヘッダはBasic認証とかでも利用されますがAPIでもこのヘッダを利用して認証トークンを送ることができます。2
トークン自体は「ぼくのかんがえたさいきょうのとーくん」ではなくちゃんとJWTなどのように安全なものを使いましょう。いつも通りFlask、といきたいところですが、FlaskにJWT機能を提供するFlask-JWTはどうにも中身が把握しきれてなくて使ったことがありません3。オレオレトークン使っちゃったりもしてます。
あとがき
以上、Web APIを作る際に考えるべきこと、バックグラウンドとなる知識、CORSや認証情報の送信などについて説明してきました。APIを作る際に参考にしていただけたら幸いです。
参考文献
Webを支える技術
もはや読んでいることが必須レベルの名著ですね。数年前の「ちょいとWebについて体系的に学び直したいな」という学習衝動が起こった時に読みました。ただ、改めて見てみるとCORSの話がない4など、「実際にAPI書くにはもう少し+αが必要」という感じでもあります。
HTTP - Wikipedia
HTTPのこれってどうだっけというときに調べるサイトその1。
HTTP | MDN
HTTPのこれってどうだっけというときに調べるサイトその2。