技術系

RESTfulなAPIへの理解

技術系

こんにちは、なかにしです。
RESTfulAPIって、どこもかしこも言われてますよね。

結構ふわっとしていて、ズバリこれです!といった説明が難しい概念だと思います。
私も正直ちゃんと理解しているとは言い難いです。

今回はそんなRESTfulなAPIについて、頑張って調べました。
かなり難しい概念になるので、解釈違いの可能性もあります。
ご了承ください。

ズバリ、何?

APIを作る際に、拡張性や保守性が高い感じで作ろうよ!って話だと思います。

ソフトウェアアーキテクチャというものがあります。

ソフトウェアの構築方法・設計スタイルのことです。
ソフトウェアアーキテクチャに従って構築・設計をすることで良いアプリができます。

ソフトウェアアーキテクチャはSOAPやRESTなど、様々な種類があります。
CSSのFROCSSとCROCSSみたいなものです。

APIは異なるプログラムがお互いに通信する為の説明書です。

エンドポイントを持つWEB APIもありますし、
エンドポイントを持たないライブラリのようなAPIもあります。

RESTというソフトウェアアーキテクチャを使用してAPIを作ろう!
がRESTfulAPIということです。

上記から分かる通り、重要なのはRESTの部分です。
以下でRESTの説明をします。

RESTについて

RESTは設計原則の1つです。
オブジェクト指向のようなもので、非常に沢山の原則や規則のまとまりです。

全ては紹介しきれないので、よく使われる一部を紹介します。

URIはリソースを表現しなければならない(リソース指向)

URIについての規約です。
URIとは、リソースを一意に識別する値です。
「http://example.com/users」の「/user」部分です。

ここは動詞ではなく名詞を使用しましょうという話です。

▽悪い例

// GET /getUsers - 全ユーザを取得
app.get('/getUsers', function(req, res) {
  // コードで全てのユーザを取得
});

// GET /getUser?id=123 - 特定のユーザを取得
app.get('/getUser', function(req, res) {
  // コードで特定のユーザを取得
});

▽良い例

// GET /users - 全ユーザを取得
app.get('/users', function(req, res) {
  // コードで全てのユーザを取得
});

// GET /users/:id - 特定のユーザを取得
app.get('/users/:id', function(req, res) {
  // コードで特定のユーザを取得
});

ステートレス

それぞれのリクエストは独立していて、サーバーはクライアントの状態を記憶しない。
つまり、リクエスト間に依存性がないということです。

要は、同じリクエストを何回送っても、
全部同じ値が返ってくるようにしろよってことです。

「”/user”に100回リクエストを送ると、99回目だけ何か違う動きになる」はアウトです。
サプライズとしてはいいと思いますが。

▽悪い例

// POST /users - 新しいユーザを作成
app.post('/users', function(req, res) {
  // 前回のリクエストから状態を取得
  const previousRequest = getPreviousRequest(req);
  // 前回のリクエストの状態に基づいて新しいユーザを作成
});

▽良い例

// POST /users - 新しいユーザを作成
app.post('/users', function(req, res) {
  // req.bodyからユーザ情報を取得し、新しいユーザを作成
  // リクエスト間に依存性がなく、このリクエストだけで完結している。
});

Client – Server構造

クライアント(フロントエンド)とサーバー(バックエンド)は独立して動作する。
クライアントはサーバーからのレスポンスを解釈し、表示する責任がある。

バックエンドはただリクエストを受け取り、データを返すだけ。
HTMLを作成して返したりはしない。
データをHTMLに表示するのはフロントの役目。

▽悪い例

// サーバーサイド
// GET /users/:id - 特定のユーザを取得
app.get('/users/:id', function(req, res) {
  // 特定のユーザを取得し、その情報をHTMLとしてレンダリング
  // サーバーがクライアントの表示方法まで決定してしまっている
});

// クライアントサイド
fetch('/users/1')
  .then(response => response.text())
  .then(html => {
    // サーバーから返されたHTMLをそのまま表示
  });

▽良い例

// サーバーサイド
// GET /users/:id - 特定のユーザを取得
app.get('/users/:id', function(req, res) {
  // 特定のユーザを取得し、その情報をレスポンスとして返す
  // サーバーはクライアントの表示方法については一切考慮しない
});

// クライアントサイド
fetch('/users/1')
  .then(response => response.json())
  .then(user => {
    // サーバーから取得したユーザ情報をどのように表示するかを決定
  });

代表的なものとして、以下の4つが挙げられます。
・GET
・POST
・PUT(PATCH)
・DELETE

これらを用途に合わせてきちんと使い分けることが必要です。

キャッシュ可能

レスポンスをキャッシュできるかどうかは、レスポンス自体で定義する。

適切に管理されたキャッシュを使用し、
クライアント-サーバー間の相互作用を部分的にまたは完全に省き、
効率とスケーラビリティを向上させる。

▽悪い例

// サーバーサイド
// GET /users/:id - 特定のユーザを取得
app.get('/users/:id', function(req, res) {
  // 特定のユーザを取得し、その情報をレスポンスとして返す
  // しかし、キャッシュに関する情報が一切ない
});

// クライアントサイド
// クライアントはこのレスポンスを受け取った際、キャッシュに関する情報がないため、キャッシュするべきか判断できない

▽良い例

// サーバーサイド
// GET /users/:id - 特定のユーザを取得
app.get('/users/:id', function(req, res) {
  // 特定のユーザを取得し、その情報をレスポンスとして返す
  // Cache-Controlヘッダを設定して、このレスポンスがキャッシュ可能であることを示す
  res.set('Cache-Control', 'public, max-age=3600');
});

// クライアントサイド
// クライアントはこのレスポンスを受け取った際、Cache-Controlヘッダを解釈し、適切にキャッシュする。

バックエンド側でレスポンスヘッダにキャッシュできるよって情報を埋め込んでフロントに渡し、
フロント側でキャッシュしています。

レイヤードシステム

クライアントはエンドポイントだけを知っていれば良く、
バックエンドの内部構造(中間層)は気にしなくて良い。

実装の隠蔽ですね。
Google Map APIを叩いたときに、どう処理されるかは理解せずとも、
何が返ってくるかが分かれば問題無いよね、って話です。

▽悪い例

// クライアントサイド
// クライアントがサーバーの内部構造を知ってしまっている
fetch('/users/1?database=main&table=users')
  .then(response => response.json())
  .then(user => {
    // サーバーから取得したユーザ情報を表示
  });

// サーバーサイド
// GET /users/:id - 特定のユーザを取得
app.get('/users/:id', function(req, res) {
  // データベースとテーブルはクライアントから指定される
  // これはレイヤードシステムの原則に反している
});

▽良い例

// クライアントサイド
fetch('/users/1')
  .then(response => response.json())
  .then(user => {
    // サーバーから取得したユーザ情報を表示
  });

// サーバーサイド
// GET /users/:id - 特定のユーザを取得
app.get('/users/:id', function(req, res) {
  // データベースからユーザ情報を取得
  // この処理は中間層であり、クライアントはこの詳細を知らなくても良い
  // クライアントはエンドポイントだけを知っていれば良い
});

統一インターフェース

同じリソースに対する操作をHTTPメソッド(GET, POST, PUT, DELETEなど)によって区別することを意味します。
これにより、APIの使用方法が単純化され、標準化されます。

取得はGET、登録はPOST…といった使い分けをちゃんとしようぜ!って話です。

▽悪い例

// サーバーサイド
// GET /getUsers - ユーザ一覧を取得
app.get('/getUsers', function(req, res) {
  // ...
});

// POST /createUser - 新しいユーザを作成
app.post('/createUser', function(req, res) {
  // ...
});

// POST /updateUser/:id - ユーザ情報を更新
app.post('/updateUser/:id', function(req, res) {
  // ...
});

// POST /deleteUser/:id - ユーザを削除
app.post('/deleteUser/:id', function(req, res) {
  // ...
});

▽良い例

// サーバーサイド
// GET /users/:id - 特定のユーザを取得
app.get('/users/:id', function(req, res) {
  // 特定のユーザを取得
});

// POST /users - 新しいユーザを作成
app.post('/users', function(req, res) {
  // 新しいユーザを作成
});

// PUT /users/:id - 特定のユーザを更新
app.put('/users/:id', function(req, res) {
  // 特定のユーザを更新
});

// DELETE /users/:id - 特定のユーザを削除
app.delete('/users/:id', function(req, res) {
  // 特定のユーザを削除
});

さいごに

「RESTfulなAPIの理解」タイトルって「知覚と快楽の螺旋」っぽいですよね。語感が。

意外と「普通にRESTに沿ってやっていた」方も多いのではと思います。
ユーザー情報取得にDELETEメソッドを使用するとかあり得ないですもんね…

実は普段から接していた設計はRESTfulだったよって話でした。
これからは「RESTfulな設計に基づいたAPIを作りました」って堂々と言っていきましょう!

今回はここまで!
Enjoy Hacking!!