- インスタンス変数
- エンティティ
- ドキュメント
- メソッドとパラメータの説明
- 破壊的な変更
- ブレークチェンジとは
- 変更でないもの
- 宣言されたパラメータ
- 配列の型
- HTTPステータスヘルパーの使用
- HTTP 動詞の選択
- GitLab Rails コードベースでの API パスヘルパーの使い方
- カスタムバリデータ
- 内部API
- N+1問題の回避
- テスト
- 変更履歴エントリを含める
APIスタイルガイド
API開発のベストプラクティスを推奨するスタイルガイドです。
インスタンス変数
インスタンス変数は使わないでください。インスタンス変数は必要ありません(Railsのビューのようにアクセスする必要はありません)。
エンティティ
エンドポイントのペイロードを提示するには、常にEntityを使用します。
ドキュメント
内部または機能フラグの後ろでない限り、新しいAPIエンドポイントや更新されたAPIエンドポイントには必ずドキュメントが必要です。ドキュメントは同じマージリクエストに含めるか、厳密に必要であれば、元のマージリクエストと同じマイルストーンでフォローアップする必要があります。
OpenAPI定義ファイルだけでなく、MarkdownでのAPIリソースのドキュメントの詳細については、ドキュメンテーションスタイルガイドRESTful APIのページを参照してください。
メソッドとパラメータの説明
すべてのメソッドは、Grape DSLを使って記述しなければなりません (よい例としてenvironments.rb
を参照ください):
-
desc
メソッドの概要のためにのような追加の詳細のためのブロックを渡す必要があります:- エンドポイントが追加された GitLab のバージョン。機能フラグの後ろにある場合は、代わりにそれを記述します:This feature is gated by the :feature_flag_symbol 機能フラグ。
- エンドポイントが非推奨である場合、その削除予定日。
-
params
メソッドのパラメータ。これは、パラメータの説明、検証、強制として機能します。
良い例は次のようなものです:
desc 'Get all broadcast messages' do
detail 'This feature was introduced in GitLab 8.12.'
success Entities::System::BroadcastMessage
end
params do
optional :page, type: Integer, desc: 'Current page number'
optional :per_page, type: Integer, desc: 'Number of messages per page'
end
get do
messages = System::BroadcastMessage.all
present paginate(messages), with: Entities::System::BroadcastMessage
end
破壊的な変更
たとえGitLabのメジャーリリースであっても、REST API v4に変更を加えてはいけません。
私たちのREST APIは、GitLabのバージョン管理とは独立した独自のバージョン管理を維持しています。現在のREST APIバージョンは4
です。私たちはREST APIのセマンティック・バージョニングに従うことを約束します。つまり、メジャーバージョンアップ(おそらく5
)が行われるまで、変更を加えることはできません。
バージョン5
は予定されていないため、まれに例外が発生することがあります。
変更を壊す代わりに後方互換性に対応
後方互換性は、多くの場合、変更された機能を古いAPIスキーマに適合させ続けることで、APIに対応させることができます。例えば、私たちの REST API はwork_in_progress
とdraft
フィールドの両方を公開しています。
例外
例外は以下の場合のみです:
- ある機能がGitLabのメジャーリリースで削除されなければならない場合。
- 後方互換性はどのような形でも維持できません。
この例外はまれであるべきです。
この例外の場合でも、フィールドや引数を削除するのではなく、必ず次のようにしなければなりません:
- フィールドに対して空の応答を返します(例えば、
"null"
や[]
)。 - 引数を no-op にします。
ブレークチェンジとは
ブレークチェンジの例をいくつか挙げます:
- フィールド、引数、列挙値の削除や名前の変更。
- エンドポイントの削除。
- 新しいリダイレクトの追加 (すべてのクライアントがリダイレクトに従うわけではありません)。
- レスポンスのフィールドタイプを変更する (たとえば、
String
からInteger
へ)。 - 新しい必須引数の追加。
- 認証、作成者、その他のヘッダー要件の変更。
-
500
以外のステータスコードの変更。
変更でないもの
壊れない変更の例
- エンドポイントの追加、必須ではない引数の追加、フィールドの追加、列挙値の追加などです。
- エラーメッセージの変更。
-
500
ステータスコードからサポートされているステータスコードへの変更 (これはバグフィックスです)。 - レスポンスで返されるフィールドの順序の変更。
宣言されたパラメータ
Grapeでは、params
ブロックで宣言されたパラメータのみにアクセスできます。渡されたけれども許可されていないパラメータをフィルタリングします。
-https://github.com/ruby-grape/grape#declared
親名前空間からパラメータを除外
既定では、declared(params)
は、すべての親ネームスペースで定義されたパラメータを含めます。
-https://github.com/ruby-grape/grape#include-parent-namespaces
ほとんどの場合、親名前空間からパラメータを除外する必要があります:
declared(params, include_parent_namespaces: false)
どのような場合にdeclared(params)
メソッド呼び出しの引数としてパラメータハッシュを渡すときには、常にdeclared(params)
を使うべきです。
インスタンスンス:
# bad
User.create(params) # imagine the user submitted `admin=1`... :)
# good
User.create(declared(params, include_parent_namespaces: false).to_h)
declared(params)
Hashie::Mash
オブジェクトを返します。このオブジェクトに対してdeclared(params)
を呼び出す必要があります。しかし、単一の要素にアクセスする場合は、params[key]
を直接使用することができます。
インスタンスンス:
# good
Model.create(foo: params[:foo])
配列の型
Grape v1.3+では、Array型はcoerce_with
ブロック、またはパラメータで定義する必要があり、APIリクエストから文字列を渡されたときに検証できません。詳しくはGrapeのアップグレードドキュメントを参照してください。
nil入力の自動強制
Grape v1.3.3以前では、nil
の値を持つArrayパラメータは自動的に空のArrayに強制されました。しかし、v1.3.3のこのプルリクエストにより、このようなことはなくなりました。たとえば、オプションのパラメータを持つ PUT/test
リクエストを定義したとします:
optional :user_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The user ids for this rule'
通常、PUT/test?user_ids
というリクエストは Grape にparams
{ user_ids: nil }
を渡します。
これは、空白の配列を期待するエンドポイントや、nil
の入力を適切に処理しないエンドポイントでエラーを引き起こす可能性があります。以前の動作を維持するために、すべてのAPIコールのbefore
ブロックで使用されるヘルパーメソッドcoerce_nil_params_to_array!
:
before do
coerce_nil_params_to_array!
end
この変更により、/test?user_ids
への PUT リクエストにより、Grape はparams
を{ user_ids: [] }
に渡します。
これを簡単にするために、Grapeのトラッカーにオープンなイシューがあります。
HTTPステータスヘルパーの使用
200 以外の HTTP レスポンスでは、not_found!
やno_content!
のように、lib/api/helpers.rb
で提供されているヘルパーを使用してください。これらのthrow
は Grape 内部でエンドポイントの実行を中断します。
DELETE
destroy_conditionally!
このヘルパーはデフォルトで成功時に204 No Content
レスポンスを返し、 与えられたIf-Unmodified-Since
ヘッダが範囲外の場合は412 Precondition Failed
レスポンスを返します。このヘルパーは渡されたリソースに対して#destroy
をコールしますが、 ブロックを渡すことで独自の削除メソッドを実装することもできます。
HTTP 動詞の選択
新しいAPIルートを定義する際には、正しいHTTPリクエストメソッドを使用してください。
PATCH
とPUT
Rails アプリケーションでは、PATCH
とPUT
の両方のリクエストメソッドがコントローラ内のupdate
メソッドにルーティングされます。GitLab API を書くために使っているフレームワークである Grape では、更新を行うエンドポイントに対してPATCH
あるいはPUT
という HTTP 動詞を明示的に設定しなければなりません。
エンドポイントが指定したリソースのすべての属性を更新する場合は、PUT
リクエストメソッドを使います。エンドポイントが与えられたリソースの一部の属性を更新する場合は、PATCH
リクエストメソッドを使います。
以下はPATCH
の良い例です: PATCH /projects/:id/protected_branches/:name
PUT
の良い例です:PUT /projects/:id/merge_requests/:merge_request_iid/approve
多くの場合、PUT
のエンドポイントは、id と動詞(上の例では “承認者”)しか持っていません。あるいは、1つの値しか持たず、キーと値のペアを表します。
Railsブログでは、更新を行うWeb APIエンドポイントでは通常PATCH
が最も適切な動詞である理由について詳しく説明しています。
GitLab Rails コードベースでの API パスヘルパーの使い方
GitLabは相対URLでのインストールをサポートしているため、Grapeが生成したAPIパスヘルパーを使うときにはこの点を考慮しなければなりません。このような API パスヘルパーの使用は、expose_path
ヘルパーの呼び出しにラップする必要があります。
インスタンスンス:
- endpoint = expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid))
カスタムバリデータ
APIリクエストに含まれるいくつかのパラメータを検証するために、それらをさらに(Gitalyに)送信する前にバリデーションを行います。以下に、これまでに追加したカスタムバリデータとその使い方を示します。また、新しいカスタムバリデータを追加する方法についても説明します。
カスタムバリデータの使用法
-
FilePath
:GitLabはファイルパスをトラバースする必要がある様々な機能をサポートしています。
FilePath
Validator は様々なケースでパラメータ値を検証します。主に、パスが相対パスかどうか、File::Separator
を使った../../
相対トラバースが含まれているかどうか、そして/etc/passwd/
のように絶対パスかどうかをチェックします。デフォルトでは、絶対パスは許可されません。しかし、オプションで、次のように絶対パスを許可する allowlist を渡すことができます:requires :file_path, type: String, file_path: { allowlist: ['/foo/bar/', '/home/foo/', '/app/home'] }
-
Git SHA
:Git SHA
バリデータ は、GitのSHAパラメータが有効なSHAであるかどうかをチェックします。commit.rb
にある正規表現を使ってチェックします。 -
Absence
:Absence
バリデータ は、指定したパラメータハッシュに特定のパラメータが存在しないかどうかを調べます。 -
IntegerNoneAny
:IntegerNoneAny
バリデータ は、指定されたパラメータの値がInteger
,None
,Any
のいずれかであるかどうかをチェックします。 このバリデータは、これらの値のいずれかだけがリクエストに進むことを許可します。 -
ArrayNoneAny
:ArrayNoneAny
バリデータ は、指定されたパラメータの値がArray
,None
,Any
のいずれかであるかどうかをチェックします。 このバリデータは、これらの値のいずれかだけがリクエストに進むことを許可します。 -
EmailOrEmailList
:EmailOrEmailList
バリデータ は、文字列あるいは文字列のリストの値が有効なメールアドレスのみを含むかどうかをチェックします。有効なメールアドレスがすべて含まれるリストだけがリクエストに進むことができます。
新しいカスタムバリデータの追加
カスタムバリデータは、パラメータをプラットフォームに送信する前にパラメータを検証するためのすばらしい方法です。無効なパラメータを最初に特定することで、サーバーからプラットフォームへのやりとりを省くことができます。
カスタムバリデータを追加する必要がある場合は、validators
ディレクトリにある専用のファイルに追加します。Grape を使って API を追加するので、バリデータクラスはGrape::Validations::Validators::Base
クラスを継承します。あとはvalidate_param!
メソッドを定義するだけです。params
ハッシュとバリデートするparam
名前の 2 つのパラメータを受け取ります。
メソッド本体はパラメータ値のバリデーションを行い、 適切なエラーメッセージを呼び出し元のメソッドに返します。
最後に、次の行でバリデータを登録します:
Grape::Validations.register_validator(<validator name as symbol>, ::API::Helpers::CustomValidators::<YourCustomValidatorClassName>)
バリデータを追加したら、validators
ディレクトリにrspec
を追加してください。
内部API
内部APIは内部使用のために文書化されています。様々なコンポーネントがどのエンドポイントを使用しているかを知るために、常に最新の状態にしておいてください。
N+1問題の回避
API エンドポイントでレコードのコレクションを返すときによくある N+1 の問題を回避するには、イーガーローディングを使用します。
API内部でこれを行う標準的な方法は、APIで返されるアソシエーションとデータをプリロードするwith_api_entity_associations
というスコープをモデルに実装することです。このスコープの例は Issue
モデル で見ることができます。
同じモデルがAPIで複数の実体を持つような場合(インスタンスンスンス:UserBasic
、User
、UserPublic
)、このスコープを適用する際には、自分の判断で行う必要があります。最も基本的なエンティティのために最適化し、後続のエンティティはそのスコープを基に構築することもできます。
with_api_entity_associations
ス コ ープは、to-dos APIで返 さ れ るTodo
_タ ーゲ ッ ト_のデー タも自動的にプ リ ロード します。
プリロードについての詳しい説明や議論は、このスコープを導入したマージリクエストを参照してください。
テストによる検証
APIエンドポイントがコレクションを返すときは、常にテストを追加して、現在も将来もAPIエンドポイントにN+1の問題がないことを検証してください。これにはActiveRecord::QueryRecorder
を使います。
使用例:
def make_api_request
get api('/foo', personal_access_token: pat)
end
it 'avoids N+1 queries', :request_store do
# Firstly, record how many PostgreSQL queries the endpoint will make
# when it returns a single record
create_record
control = ActiveRecord::QueryRecorder.new { make_api_request }
# Now create a second record and ensure that the API does not execute
# any more queries than before
create_record
expect { make_api_request }.not_to exceed_query_limit(control)
end
テスト
新しい API エンドポイントのテストを書く際には、/spec/fixtures/api/schemas
にあるスキーマフィクスチャを使うことを検討してください。 与えられたスキーマにマッチするレスポンスをexpect
で指定できます:
expect(response).to match_response_schema('merge_requests')
テストにおけるN+1 パフォーマンスの検証も参照してください。
変更履歴エントリを含める
クライアント向けのすべての変更には、変更履歴エントリを含める必要があります。内部APIは含まれません。