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_progressdraft フィールドの両方を公開しています。

例外

例外は以下の場合のみです:

  • ある機能が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)
note
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リクエストメソッドを使用してください。

PATCHPUT

Rails アプリケーションでは、PATCHPUT の両方のリクエストメソッドがコントローラ内の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で複数の実体を持つような場合(インスタンスンスンス:UserBasicUserUserPublic )、このスコープを適用する際には、自分の判断で行う必要があります。最も基本的なエンティティのために最適化し、後続のエンティティはそのスコープを基に構築することもできます。

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は含まれません。