⚔️

【Web API 設計 〜 実践編 〜】RESTの核心に迫る!REST ful APIを実装する

2024/03/15に公開

こんにちは、AIQ株式会社のフロントエンドエンジニアのまさぴょんです!
前回は、Web APIの設計思想である、RESTと、そこから発展したROAについて解説しました。
今回は、REST, ROAのポイントを実践する形で、RESTful Web APIのSampleを作成したので、そちらをご紹介します。

前回の記事は、こちらです👀✨
https://zenn.dev/aiq_dev/articles/48100d5b3f13fe

今回、作成した RESTful Web API の Sample Project は、こちらです🌟
https://github.com/yukimura-manase/restful-web-api

前回のあらすじ 〜 RESTとROAに観るWeb APIの設計思想 〜

Web API 設計の実践編に入る前に、前回の記事の振り返りをします。

RESTとは?

REST(REpresentational State Transfer)とは、Webサービスのアーキテクチャスタイル(設計モデル・設計思想)の1つです。
RESTにおける重要概念として 「リソース」(Resource)「URI」 があります。
RESTful Web API では、Client/Server 間でやり取りする「リソース」(Resource)が主役であり、その「リソース」(Resource)に対する適切な「URI」の設計をすることが重視されています。
RESTにとって、「URI」とは、「リソース」(Resource)の表現したものだと言えます。

RESTを構成する6つのアーキテクチャスタイル(設計思想)

RESTを構成する6つのアーキテクチャスタイル(設計思想)

RESTは、次の6つのアーキテクチャスタイル(設計思想)で構成されるアーキテクチャスタイル(設計思想)です。

  1. Client/Server(クライアント-サーバー)
    • クライアントはサーバーにリクエストを送り、サーバーはレスポンスでリソースを返すという関係性
  2. Stateless(ステートレス)
    • サーバー側がクライアント・アプリケーションの状態(State)を保持・管理しないことを意味する
  3. Cache(キャッシュ)
    • 一度取得したリソースをクライアント側で使い回す
  4. Uniform Interface(統一したインターフェース)
    • URIで示したリソースに対する操作を、統一した限定的なインターフェースで実施する
    • HTTPプロトコルのメソッドで、CRUDを表現する
      • 「どのリソースであっても GET は読み取り・取得を意味する」という統一性を持たせる
  5. Layered System(階層システム)
    • レイヤー構成されたアーキテクチャスタイルのこと
    • 各レイヤーは特定の機能と役割を持っており、各レイヤーは、独立しながら相互作用を有する
  6. Code-On-Demand(オンデマンドのコード)
    • プログラムコードをサーバーからダウンロードして、クライアント側でそれを実行するアーキテクチャスタイル
    • 唯一のオプショナルな制約
    • コードの送信によるセキュリティの懸念が生じるため、採用は進んでいません。。。🥺

ROA(リソース指向アーキテクチャ)とは?

ROAとは、Resource Oriented Architectureの略で、日本語に訳すと「リソース指向アーキテクチャ」といいます。
ROAは、RESTから発展した設計思想であり 「RESTful Web Services」という書籍にて、提唱されました。
ROAは、名前の通り 「リソース」 に重点を置いたアーキテクチャで、RESTful な設計思想です。
リソースは ROA だけでなく REST において最も重要な概念であり、リソースとは、サービスが提供する「もの」を指します。

RESTを構成する6つのアーキテクチャスタイル(設計思想)

「リソース指向アーキテクチャ」(ROA)の4つの特性

「リソース指向アーキテクチャ」(ROA)には、次の4つの特性があるとされています。

  1. アドレス可能性(Addressability)
    • リソースを URI で一意に識別して、API としてアクセス可能であることを指します
    • URIを通じて、リソースを取得する簡単にアクセスできる状態になっていることを意味します
  2. ステートレス性(Statelessness)
    • ステートレスなAPIでは、 Stateを保持せず「やりとりが1回ごとに完結する」 ようになっています
  3. 統一インターフェース(Uniformed Interface)
    • すべてのサービスが HTTP のインターフェースを同じ方法で使用するという特性
    • HTTPプロトコルのメソッドで、CRUDを表現する
      • 「どのリソースであっても GET は読み取り・取得を意味する」という統一性を持たせる
  4. 接続性(Connectedness)
    • リソースに別のリソースへのリンクを含めて接続することができるという特性
    • リソースどうしが適切に接続されているアクセスできるような状態

RESTfulなWeb APIの設計をする

RESTとROAという、Web API 設計の理論・考え方は、わかりましたが、実際に、Codeで表現して理解を深める必要があります。
そこで、RESTful Web APIのSample Projectを作ったので、そのポイントについて解説していきます。

今回は、誰でも遭遇するような 「User に関する CRUD 処理を RESTful Web API として実装すると、どうなるか?」 というテーマで、RESTfulなWeb APIの設計と実装をしていきます。

URIの設計を実施する: アドレス可能性

RESTや、ROAにとって「URI」は 「リソース」(Resource) を表現しています。
つまり「URI」を見れば「リソース」(Resource)が何なのか、すぐに判断できる状態がいい設計だと言えます。

また、URIの設計をする際のポイントには、主に次の2つの注意点があります。

  1. URIは 「リソース」(Resource) に適した形の名詞で表現する
    • 「アクション」(動作)ではなく「リソース」(もの)なので、名詞で表現します。
  2. URIは複数形で表現する
    • 「リソース」(Resource)は、複数になりうる可能性を持っているので、複数形で表現する。

ここら辺のURIの表現が、どうあるべきかの説明は、こちらの記事がわかりやすいです。
https://qiita.com/ryo88c/items/0a3c7861015861026e00

CRUD処理のエンドポイントを設計する: 統一インターフェース

URIが決定したところで、CRUD処理のエンドポイントを設計していきます。
ちなみに、CRUDとは、新規作成(Create), 読み取り(Read), 更新(Update), 削除(Delete)の4つの頭文字をとった略語です。
今回は、User情報の取得、新規作成、更新、削除に関連するエンドポイントを設計します。
エンドポイントの内容は、次のとおりです。

  1. Create: User 新規作成
    • POST 通信: /users
      • User データを新規作成する
      • User 情報は、リクエストボディに含めます。
  2. Read: User 情報・取得(Userの一覧取得と特定のUser取得)
    • GET 通信: /users
      • User の一覧データを取得する
    • GET 通信: /users/{user_id}
      • 特定(user_id)の User の情報を取得します。
  3. Update: User 情報・更新 (部分的・Update)
    • PATCH 通信: /users/{user_id}
      • 特定(user_id)の User の情報を部分的に Update します。
      • Update する情報をリクエストボディに含めます。
  4. Delete: User 削除
    • DELETE 通信: /users/{user_id}
      • 特定(user_id)の User の情報を削除する

POST, PUT, PATCHの使い分けについて

POST, PUT, PATCHの使い分けについて、要点をまとめると次のとおりです。

  1. POSTは、リソースを新しく使用する際に使用する
    • 基本的には、新規作成のみで使用する
    • 例外として、PUTやPATCHを使用しないAPIなどでは、POSTで、Updateを実施する場合もある
  2. PUTは、既存リソースの新しいものに置き換える際に使用する
    • リソース全体をUpdate(置換)する役割で使用する
    • 新しいリソースの中に既存リソースにある要素がない場合、その要素は削除される
  3. PATCHは、既存リソースに新しいものを付け足したり、Updateする際に使用する
    • 既存のリソースの一部分だけを更新する or リソースを一部、追加する役割で使用する
    • 新しいリソースの中に既存リソースにある要素がない場合でもその要素は削除されない

https://zenn.dev/sgtkuc1118/articles/c73587c674a4a2

RESTfulなWeb APIの実装をする

設計も済んだところで、RESTfulなWeb APIの実装をしていきます。

使用技術

今回、Sample Projectで使用するのは、Expressになります。
また、API 設計のポイントを学ぶための Sample プロジェクトなので、DBは使用せず、JSON ファイルを簡易的な DataStore として用意しました。

今回、作成した RESTful Web API の Sample Project は、こちらです🌟
https://github.com/yukimura-manase/restful-web-api

Create: Userを新規作成する

まずは、User 新規作成の処理を作成します。
/users のエンドポイントに対する Post 通信では User の新規登録を受け付けるようにします。
処理の内容を要約すると、次のようなことをしています。

  1. バリデーション Check  を実施する
    • user_id と password がない場合は、400 Bad Request を返す。
    • user_id が 6 文字以上 20 文字以内の半角英数字であるかどうかを確認する。
    • password が 8 文字以上 20 文字以内の半角英数字であるかどうかを確認する。
    • user_id が既に存在する場合(User 登録がある場合)は、400 を返す。
  2. 上記のエラーパターンに該当しなければ、 Account を作成して、登録完了のレスポンスを返す。
// User データの新規登録エンドポイント: Create
app.post("/users", (req, res) => {
  // HTTPリクエストのボディを出力
  console.log(req.body);
  console.log("POSTリクエストを受け取りました");

  // 0. user_id と password を取得する
  const user_id = req.body?.user_id ?? false;
  const password = req.body?.password ?? false;

  console.log("user_id:", user_id);
  console.log("password:", password);

  // 1. user_id と password がない場合は、400 Bad Request を返す。
  if (!user_id || !password) {
    res.status(400).json({
      message: "Account creation failed",
      cause: "required user_id and password",
    });
    console.error({
      message: "Account creation failed",
      cause: "required user_id and password",
    });
    return;
  }

  // 2. user_id が 6 文字以上 20 文字以内の半角英数字であるかどうかを確認する。
  if (!/^[a-zA-Z0-9]{6,20}$/.test(user_id)) {
    res.status(400).json({
      message: "Account creation failed",
      cause: "invalid user_id",
    });
    return;
  }

  // 3. password が 8 文字以上 20 文字以内の半角英数字であるかどうかを確認する。
  if (!/^[a-zA-Z0-9]{8,20}$/.test(password)) {
    res.status(400).json({
      message: "Account creation failed",
      cause: "invalid password",
    });
    return;
  }

  // 4. nickname と comment を取得する: ない場合は、空文字を代入する
  const nickname = req.body.nickname ?? "";
  const comment = req.body.comment ?? "";

  // 5. user_id が既に存在する場合は、400 を返す
  const user = dataStore.find((user) => user.user_id === user_id);
  if (user) {
    res.status(400).json({
      message: "Account creation failed",
      cause: "already same user_id is used",
    });
    return;
  }

  // 6. アカウントを新規作成する (dataStore に追加する)
  dataStore.push({ user_id, password, nickname, comment });

  // 7. dataStore を data-store.json に保存する
  fs.writeFileSync(dataStorePath, JSON.stringify(dataStore, null, 2), "utf8");

  // Account 登録完了のレスポンスを返す
  res.json({
    message: "Account successfully created",
    data: {
      user: {
        user_id,
        nickname,
      },
    },
  });
});

Read: すべてのUserの情報を取得する

続いて、Userの一覧取得処理を実装していきます。
/users のエンドポイントに対する Get 通信では User データの一覧取得を受け付けるようにします。
処理の内容を要約すると、次のようなことをしています。

  1. User の新規登録処理、以外は、Request User が認証ユーザーがどうか? を Check する
    • authorization ヘッダーを取得して、Base64 エンコードをデコードして、user_idpasswordを取得する
    • 認証ユーザーからの Request ではない場合は、401 Unauthorized を返す。
    • 認証ユーザーの場合は、そのまま処理を続行する。
  2. User 一覧を返却する。
// User データの取得: Read Ver. すべての User の情報を取得する
app.get("/users", (req, res) => {
  console.log("GETリクエストを受け取りました");

  // 1. authorization ヘッダーを取得する
  const authorization = req.headers.authorization;
  console.log("authorization:", authorization);

  // 2. authorization ヘッダーから、Base64 エンコードされた文字列を取得する ( user_id:password でエンコードされている)
  const base64 = authorization.split(" ")[1];
  console.log("base64:", base64);

  // 3. Base64 エンコードされた文字列をデコードする
  const decoded = Buffer.from(base64, "base64").toString("utf-8");
  console.log("decoded:", decoded);

  // 4. デコードされた文字列を user_id と password に分割する
  const [user_id, password] = decoded.split(":");

  console.log("user_id:", user_id);
  console.log("password:", password);

  // 5. user_id と password が一致するデータが dataStore にあるかどうかを確認する
  const authUser = dataStore.find(
    (user) => user.user_id === user_id && user.password === password
  );
  console.log("authUser:", authUser);

  // 6. 認証ユーザーでない場合は、401 Unauthorized を返す
  if (!authUser) {
    // 認証ユーザーが見つからない場合は、401 Unauthorized を返す
    res.status(401).json({ message: "Authentication Failed" });
    return;
  }

  /** 7. User 一覧データ (Password の情報は含めない) */
  const users = dataStore.map((user) => {
    return {
      user_id: user.user_id,
      nickname: user.nickname,
      comment: user.comment,
    };
  });
  console.log("users:", users);

  // レスポンスを返す
  res.json({
    message: "All User details",
    data: users,
  });
});

Read: 特定のUser情報を取得する

続いて、特定のUserを取得する処理を実装していきます。
/users/:user_idのエンドポイントに対する Get 通信では、特定の User データの取得を受け付けるようにします。
処理の内容を要約すると、次のようなことをしています。

  1. User の新規登録処理、以外は、Request User が認証ユーザーがどうか? を Check する
    • authorization ヘッダーを取得して、Base64 エンコードをデコードして、user_idpasswordを取得する
    • 認証ユーザーからの Request ではない場合は、401 Unauthorized を返す。
    • 認証ユーザーの場合は、そのまま処理を続行する。
  2. user_id を Query Parameter として受け取る
  3. 取得 Request の User が存在しない場合は、404 エラーを返す。
  4. User データが存在する場合は、これを返却する。
// User データの取得: Read Ver. 特定の User の情報を取得する
app.get("/users/:user_id", (req, res) => {
  console.log("GETリクエストを受け取りました");

  // 1. authorization ヘッダーを取得する
  const authorization = req.headers.authorization;
  console.log("authorization:", authorization);

  // 2. authorization ヘッダーから、Base64 エンコードされた文字列を取得する ( user_id:password でエンコードされている)
  const base64 = authorization.split(" ")[1];
  console.log("base64:", base64);

  // 3. Base64 エンコードされた文字列をデコードする
  const decoded = Buffer.from(base64, "base64").toString("utf-8");
  console.log("decoded:", decoded);

  // 4. デコードされた文字列を user_id と password に分割する
  const [user_id, password] = decoded.split(":");
  console.log("user_id:", user_id);
  console.log("password:", password);

  // 5. user_id と password が一致するデータが dataStore にあるかどうかを確認する
  const authUser = dataStore.find(
    (user) => user.user_id === user_id && user.password === password
  );
  console.log("authUser:", authUser);

  // 6. 認証ユーザーでない場合は、401 Unauthorized を返す
  if (!authUser) {
    // 認証ユーザーでない場合は、401 Unauthorized を返す
    res.status(401).json({ message: "Authentication Failed" });
    return;
  }

  // 7. user_id を取得する
  const targetUserId = req.params.user_id;
  console.log("targetUserId:", targetUserId);

  // 8. dataStore から user_id に一致するデータを取得する
  const user = dataStore.find((user) => user.user_id === targetUserId);
  console.log("user:", user);

  let response = {};

  // 9. 該当のユーザーが見つかった場合は、該当のユーザーの情報を返す
  if (user) {
    response = {
      message: "User details by user_id",
      // Password 以外の User データを返却する
      data: {
        user_id: user.user_id,
        nickname: user.nickname,
        comment: user.comment,
      },
    };
    // レスポンスを返す
    res.json(response);
  } else {
    response = {
      message: "No User found",
    };
    res.status(404).json(response);
  }
});

Update: User情報を更新する(部分的・Update)

User 情報の更新処理を実装します。
今回は、password, nickname, comment に関して、部分的に Updateするようなエンドポイントとするので、HTTPメソッドは、PATCHを使用します。
/users/:user_id のエンドポイントに対する PATCH 通信では User データの一部の更新を受け付けるようにします。
処理の内容を要約すると、次のようなことをしています。

  1. User の新規登録処理、以外は、Request User が認証ユーザーがどうか? を Check する
    • authorization ヘッダーを取得して、Base64 エンコードをデコードして、user_idpasswordを取得する
    • 認証ユーザーからの Request ではない場合は、401 Unauthorized を返す。
    • 認証ユーザーの場合は、そのまま処理を続行する。
  2. user_id を Query Parameter として受け取る
  3. 該当 User の password, nickname, comment を更新する
// User データの更新: Update
app.patch("/users/:user_id", (req, res) => {
  console.log("User Update Request");

  // 1. authorization ヘッダーを取得する
  const authorization = req.headers.authorization;
  console.log("authorization:", authorization);

  // 2. authorization ヘッダーから、Base64 エンコードされた文字列を取得する ( user_id:password でエンコードされている)
  const base64 = authorization.split(" ")[1];
  console.log("base64:", base64);

  // 3. Base64 エンコードされた文字列をデコードする
  const decoded = Buffer.from(base64, "base64").toString("utf-8");
  console.log("decoded:", decoded);

  // 4. デコードされた文字列を user_id と password に分割する
  const [user_id, password] = decoded.split(":");
  console.log("user_id:", user_id);
  console.log("password:", password);

  // 5. user_id と password が一致するデータが dataStore にあるかどうかを確認する
  const authUser = dataStore.find(
    (user) => user.user_id === user_id && user.password === password
  );
  console.log("authUser:", authUser);

  // 6. 認証ユーザーでない場合は、401 Unauthorized を返す
  if (!authUser) {
    // 認証ユーザーでない場合は、401 Unauthorized を返す
    res.status(401).json({ message: "Authentication Failed" });
    return;
  }

  /** Update Target User Id */
  const targetUserId = req.params.user_id;
  const targetPassword = req.body.password;
  const nickname = req.body.nickname;
  const comment = req.body.comment;
  console.log("targetUserId:", targetUserId);
  console.log("nickname:", nickname);
  console.log("comment:", comment);

  // 7. 該当のユーザーが見つかった場合は、nickname, password, comment を更新する
  dataStore = dataStore.map((user) => {
    if (user.user_id === targetUserId) {
      user.nickname = nickname;
      user.password = targetPassword;
      user.comment = comment;
    }
    return user;
  });

  // 8. dataStore を data-store.json に保存する
  fs.writeFileSync(dataStorePath, JSON.stringify(dataStore, null, 2), "utf8");

  // 9. 更新完了のレスポンスを返す
  res.json({ message: "User details successfully updated" });
});

Delete: User情報を削除する

最後に、User情報の削除処理を実装します。
/users/:user_id のエンドポイントに対する Delete 通信では User データの削除を受け付けるようにします。
処理の内容を要約すると、次のようなことをしています。

  1. User の新規登録処理、以外は、Request User が認証ユーザーがどうか? を Check する
    • authorization ヘッダーを取得して、Base64 エンコードをデコードして、user_idpasswordを取得する
    • 認証ユーザーからの Request ではない場合は、401 Unauthorized を返す。
    • 認証ユーザーの場合は、そのまま処理を続行する。
  2. user_id を Query Parameter として受け取る
  3. 該当 User のデータを削除する
// User データの削除: Delete
app.delete("/users/:user_id", (req, res) => {
  console.log("User Delete Request");

  // 1. authorization ヘッダーを取得する
  const authorization = req.headers.authorization;
  console.log("authorization:", authorization);

  // 2. authorization ヘッダーから、Base64 エンコードされた文字列を取得する ( user_id:password でエンコードされている)
  const base64 = authorization.split(" ")[1];
  console.log("base64:", base64);

  // 3. Base64 エンコードされた文字列をデコードする
  const decoded = Buffer.from(base64, "base64").toString("utf-8");
  console.log("decoded:", decoded);

  // 4. デコードされた文字列を user_id と password に分割する
  const [user_id, password] = decoded.split(":");
  console.log("user_id:", user_id);
  console.log("password:", password);

  // 5. user_id と password が一致するデータが dataStore にあるかどうかを確認する
  const authUser = dataStore.find(
    (user) => user.user_id === user_id && user.password === password
  );
  console.log("authUser:", authUser);

  // 6. 認証ユーザーでない場合は、401 Unauthorized を返す
  if (!authUser) {
    res.status(401).json({ message: "Authentication Failed" });
    return;
  }

  /** Delete Target User Id */
  const targetUserId = req.params.user_id;
  console.log("targetUserId:", targetUserId);

  // 7. 該当するユーザーを dataStore から削除する
  dataStore = dataStore.filter((user) => user.user_id !== targetUserId);

  // 8. dataStore を data-store.json に保存する
  fs.writeFileSync(dataStorePath, JSON.stringify(dataStore, null, 2), "utf8");

  // 9. User 削除完了のレスポンスを返す
  res.json({ message: "User Account successfully removed" });
});

まとめ・感想

理論の理解と、実践は合わせて実施したことで、RESTとROAについて、体系的に学ぶことができました。

Xやっております!よかったらフォローしてください🐱
https://twitter.com/masanyon1212

注意事項

この記事は、AIQ 株式会社の社員による個人の見解であり、所属する組織の公式見解ではありません。

求む、冒険者!

AIQ株式会社では、一緒に働いてくれるエンジニアを絶賛、募集しております🐱🐹✨

詳しくは、Wantedly (https://www.wantedly.com/companies/aiqlab)を見てみてください。

参考・引用

https://zenn.dev/aiq_dev/articles/48100d5b3f13fe

https://github.com/yukimura-manase/restful-web-api

https://qiita.com/ryo88c/items/0a3c7861015861026e00

https://qiita.com/suin/items/316cb8aaf8dfcf11abae

https://qiita.com/suin/items/d17bdfc8dba086d36115

https://zenn.dev/sgtkuc1118/articles/c73587c674a4a2

AIQ Tech Blog (有志)

Discussion