LoginSignup
30
24

More than 3 years have passed since last update.

REST原則に従った非同期APIの設計

Last updated at Posted at 2019-09-12

はじめに

ある程度時間のかかる重い処理をするAPIを同期的に処理をしてしまうと、そのリクエストがスレッドを占有してしまい、他の軽いAPIに影響を与えてしまうことがあります。そこで、先日、RESTに従った非同期なAPIを作りたくて調べていた時に、皆それぞれ独自に設計をしていて、どう設計するのが良いのか迷ったため、その時に考察したことをまとめます。

同期的に処理をする場合

まず、RESTに従った同期APIの設計について考えます。ここではユーザを作成する場合を例にしました。同期APIについては、ほとんどの場合、以下のような設計になっていました。

はじめに、/users に対して、POSTリクエストを送ります。レスポンスにはステータスコード201 Createdに、HTTPヘッダーのLocationには作られたリソースへ参照するためのパスが入ります。

$ curl -v -X POST -H "Content-Type: application/json" -d '{"name": "yuto425","gender":"male"}' http://api.example.com/users
< HTTP/1.1 201 Created
< Location: /users/<user_id>
<

Locationヘッダーのパスにアクセスすると、作られたユーザの情報が取得できます。

$ curl -v -X GET http://api.example.com/users/<user_id>
< HTTP/1.1 200 OK
<
{
    "id": <user_id>,
    "name": "yuto425",
    "gender": "male"
}

非同期に処理をする場合

はじめのリクエスト

同期的に処理をする場合は、多くの場合上記のような設計がされているようでしたが、非同期の場合はどのようにするのが良いでしょうか。調べていくと、以下のようにステータスコード202 Acceptedと、HTTPヘッダーのLocationにバックグラウンド処理のステータスを参照するためのパスを入れるケースが多く見受けられました。ステータスコード202 Acceptedはリクエストを受け取ったが処理はされていないということを表します。

$ curl -v -X POST -H "Content-Type: application/json" -d '{"name": "yuto425","gender":"male"}' http://api.example.com/users
< HTTP/1.1 202 Accepted
< Location: /tasks/<task_id>
<

例えば、以下など

ステータスコード 202 Acceptedについては問題ないかと思いますが、Locationヘッダーを使うことが正しいのか疑問に思うところがあったので、調べていくと以下のDelmo氏の回答が参考になりました。ここでは「ステータスコード 202 AcceptedLocationヘッダーを使うのはRFCに準拠していないため、含めるべきではない。」と書いてあります。ここで 202 AcceptedLocationをつけると言うことは、このLocationヘッダーはRFCに準拠したヘッダーではなく、独自ヘッダーと言うことになります。

他の方法としては、レスポンスのBodyや独自ヘッダーにIDを含める方法がありました。

レスポンスBodyにIDを含める方法

$ curl -v -X POST -H "Content-Type: application/json" -d '{"name": "yuto425","gender":"male"}' http://api.example.com/users
< HTTP/1.1 202 Accepted
<
{
    "task_id": <task_id>
}

独自ヘッダーにIDを含める方法

$ curl -v -X POST -H "Content-Type: application/json" -d '{"name": "yuto425","gender":"male"}' http://api.example.com/users
< HTTP/1.1 202 Accepted
< X-Task-Id: <task_id>
<

Location ヘッダーを用いる方法はRFCに準拠した201 Createdとは違い、独自ヘッダーであるにも関わらず、そのように見えないので、誤解を招く恐れがあるので、個人的には使わない方が良いかと思いました。後の2つに関してはどちらでも良いかと思いますが、後述にレスポンスBodyに情報を入れるため、ここは揃えるために今回はレスポンスBodyにIDを入れる方法を選択しました。

処理待ち中のリクエスト

ユーザ情報作成のステータスを確認するためのリクエストを、先ほど取得したタスクのIDを元に送ります。結論から言うと以下のように設計しました。上からユーザ作成が処理中、処理に失敗した場合、処理が完了した場合のレスポンスです。resultの中にそれぞれで必要な情報を含みます。

$ curl -v -X GET http://api.example.com/tasks/<task_id>
< HTTP/1.1 200 OK
<
{
    "status": "IN_PROGRESS",
    "progress_percentage": 30,
    "accepted_at": 2017-10-13T17:23:46.000Z,
    "started_at": 2017-10-13T17:23:50.000Z,
    "finished_at":  null,
    "result": null,
}
$ curl -v -X GET http://api.example.com/tasks/<task_id>
< HTTP/1.1 200 OK
<
{
    "status": "FAILED",
    "progress_percentage": null,
    "accepted_at": 2017-10-13T17:23:46.000Z,
    "started_at": 2017-10-13T17:23:50.000Z,
    "finished_at":  null,
    "result": {
        "message": "error message is here"
    }
}
$ curl -v -X GET http://api.example.com/tasks/<task_id>
< HTTP/1.1 200 OK
<
{
    "status": "SUCCEEDED"
    "progress_percentage": 100,
    "accepted_at": 2017-10-13T17:23:46.000Z,
    "started_at": 2017-10-13T17:23:50.000Z,
    "finished_at": 2017-10-13T17:23:30.000Z,
    "result": {
        "user_id": <user_id>
    }
}

以下のサイトではユーザ作成が完了した場合、処理中や処理失敗時と同様に200 OKを返すのではなく、303 See Other を使って、作られたユーザにリダイレクトをする方法が取られていました。ここにおいては、GET /tasks/<task_id>に対してリクエストをした場合に返すべきは、そのtaskがどうなっているかであって、その結果生まれた副産物を返すのはRESTの原則としてどうなのかと思い、今回は却下しました。

最後のリクエスト

最後は受け取ったGET /users/<user_id>に対してリクエストを送って、作られたリソースを取得するだけです。

$ curl -v -X GET http://api.example.com/users/<user_id>
< HTTP/1.1 200 OK
<
{
    "id": <user_id>,
    "name": "yuto425",
    "gender": "male"
}

まとめ

調べていると色々な方法があったのですが、一番しっくりきた方法を今回は選びました。もっと良い方法があれば、是非教えていただきたいです。

参考

30
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
24