web-dev-qa-db-ja.com

REST APIデザインパターン

Node.js&Express.jsを使用してREST AP​​Iを開発しています。最近、REST AP​​Iのデザインのベストプラクティスを探し始めましたが、少しわかりにくいので、ご容赦ください。

例:

次のAPIコレクションがあるとします。

  1. ユーザー
  2. タスク(すべてのタスクはユーザーに属している必要があります)

そして、これらのリソースを持っている:

  • GET /api/usersはすべてのユーザーを返します
  • GET /api/users/:idは特定のユーザーを返します
  • POST /api/usersは単一のユーザーを作成します
  • POST /api/users/:idは特定のユーザーを更新します
  • POST /api/tasksは新しいタスクを作成しますが、loginInユーザーに割り当てます。
  • POST /api/tasks/:idは特定のタスクを更新します
  • GET /api/tasksはすべてのタスクを取得します
  • GET /api/users/:id/tasksは、特定のユーザーのすべてのタスクを取得します
  • GET /api/users/:id/tasks/:idは、特定のユーザーの特定のタスクを取得します

これで、上記の各"Resource"は、実際にアクションを実行する"Controller"にリダイレクトされます。

例えば:

POST ('/api/users', myController)

let myController = (req,res) => {
    Create new User
    Save to Database
    Call Private Send an Email to Admin function etc
 }

質問:

filterなどのリソースに対していくつかのクエリを実行する必要があるとしましょう。

オプション1:

So /api/users/:id/tasks/filterのような新しいリソースをコントローラ内に作成し、コントローラ内のデータベースでフィルタリングアクションを実行します

オプション2:

So /api/users/:id/tasks?filter=title::someValue&priority::someOtherValueのようなクエリ文字列を使用します。これを行う私の知識によると[〜#〜] not [〜#〜]新しいリソースを作成しますが、上記のクエリ文字列で/api/users/:id/tasksリソースを呼び出すだけです。

オプション2に従うと、/api/users/:id/tasksアクションを管理するコントローラーが1つだけになるため、このリソースのクエリを処理するには、次のようにします。

GET ('/api/users/:id/tasks', myController)

let myController = (req,res) => {

    if(req.filter)
    {handle Query here and get data from database and return to user}

    else if(some other query)
    {handle query here}

    else if(some other query)
    {handle query here}

    else {
      if there are NO queries then get get all Tasks for a specific User.
    }

 }

上記の実装/概念は正しいですか?私にはそれは厄介で混乱しているように見えます。それで、私はここで何が間違っているのですか?クエリを処理する正しい方法は何ですか?リソースに対して単一のコントローラが必要ですか?その特定のリソースのすべてを処理しますか?

「REST」で考えるのに苦労しているので情報が多すぎて何が間違っているのかわかりません。ですから、私の基本は少し弱いので、それに関するガイダンスは非常に役立ちます(記事やオンラインリソース)。

免責事項

質問の対象はRESTですが、RESTとは何の関係もありません。

要するに、リクエストパラメータを特定のクエリ言語にマップする方法は、実装の詳細です。したがって、今のところ、RESTはそのままにしておきます。


上記の実装/コンセプトは正しいですか?

正しいかどうかを言うには、プロジェクトとその要件に精通している必要があります。これまでのところ、私たちが言えることは次のとおりです:解は柔軟ではありません

長期的にはスケーリングの意味で柔軟です。新しいfilterはすべて、(少なくとも)controller、サービスレイヤー、そしておそらくデータアクセスレイヤーも変更する必要があります。管理の観点からは、機能が少ないために多くの作業を購入するのは困難です。言い換えれば、メンテナンスは高価に見えます。

それでも、クエリに新しいパラメーターを追加する頻度によって異なります。

だから私はここで何が間違っているのですか?

2つの可能な抽象化がありません。

  1. 要求パラメーターの動的セットをAPIの特定のモデルにマップするもの。
  2. 前のモデルを特定の クエリ言語 にマップするもの。

:一部のフレームワークでは#1がオプションになります。

現在、マッピングはif/elseブロックとしてハードコードされています。うまくスケーリングするには硬すぎます。

代わりに、次のようになります。

myController(req,res){

   var query = new QueryParametersMapper(req);

   var page = new PageParametersMapper(req);

   var sort = new SortParametersMapper(req);

   var dataSet = myRepository.find(query, page, sort);

  // etc...
}

クエリを処理する正しい方法は何ですか?

マッピングをより動的で再利用可能にする

この時点で、私の最初のアドバイスは、フレームワークまたはプログラミング言語と互換性のあるライブラリを探すです。

2番目のアドバイスは、Convention over configurationをサポートするものを選択することです。

変更が妨げられない場合でも、少なくとも(確かに)変更のコストは現在よりも少なくなります。*


*質問の例として挙げられているコードを検討した場合。

1
Laiv

RESTful API開発者としての私の意見を以下に示します。これらの一部は、RESTとは関係ありませんが、HTTPの適切なプラクティスです。

  • オプション#2を使用し、クエリ文字列を使用します。これは、ブラウザのキャッシュなどの仲介者に適しています。
  • ?filter=title::someValue&priority::someOtherValueの代わりに?title=someValue&priority=someOtherValueをお勧めします
  • 意図したとおりに?filter=title::someValue&priority::someOtherValueを使用するには、とにかくアンパサンドをURLエンコードする必要があります。
  • URLに/api/を含めないことを強くお勧めします。人間を対象としたAPIエンドポイントと、URLに依存しない表現以外のマシンを対象としたAPIエンドポイントの間に違いはありません。 URLを共有すると、同じリンクを人間またはマシンがたどることができ、それぞれが適切な表現を取得できます。 /users/:idがHTMLページを返し、/api/user/:idがJSONを返す場合、これらのURLは両方とも同じことを表しているため(技術的には間違いではありません。複数のURLが完全に同じリソースを特定します—しかし、それはより良いかもしれません)。
  • 同様に、個々のタスクに/api/users/:id/tasks/:idを使用せず、その下に/tasksコレクションと/tasks/:idがあり、/users/:id/tasksから直接後者にリンクしています。/tasksとその下にあるすべてのものは、/ usersと同じようにアクセス制御されます。

あなたも言う:

これを行う私の知識によれば、新しいリソースは作成されません

それは絶対にあります!すべての固有のURLは異なるリソースです。別のURLと同じコンテンツを返すか、別のURLにリダイレクトするか、他のURLに<link rel=canonical>を持っているか、別のURLのサブセットであるかは関係ありません。 URLは「リソース」の抽象的な概念を識別します。その一部はバイトストリームとして表現でき、一部は表現できません。

0
Nicholas Shanks