コンテンツにスキップ

Faunaを使用してサーバーレスでグローバルに分散したREST APIを作成する

Last reviewed: 18 days ago

このチュートリアルでは、Faunaをデータレイヤーとして使用して、在庫カタログを管理するREST APIを構築することにより、Cloudflare Workersアプリケーションでデータを保存および取得する方法を学びます。

学習目標

  • WorkersでFaunaからデータを保存および取得する方法。
  • Wranglerを使用して秘密情報を安全に保存する方法。
  • HonoをWorkersのWebフレームワークとして使用する方法。

Fauna、Workers、およびHonoを使用して、単一のリポジトリ内でグローバルに分散し、強く一貫性のある完全なサーバーレスREST APIを作成できます。

Faunaは柔軟なスキーマを持つドキュメントベースのデータベースです。これにより、データの構造を定義し、その構造に従ったドキュメントを保存できます。このチュートリアルでは、各productドキュメントが以下のプロパティを含む製品在庫を構築します。

  • title - 製品のタイトルまたは名前を表す人間に優しい文字列。
  • serialNumber - 製品を一意に識別する機械に優しい文字列。
  • weightLbs - 製品の重量をポンドで表す浮動小数点数。
  • quantity - 在庫内の特定の製品のアイテム数を表す非負整数。

ドキュメントはコレクションに保存されます。ドキュメントデータベースのコレクションは、関連するドキュメントのグループです。

このチュートリアルでは、すべてのAPIエンドポイントは公開されています。ただし、Faunaはエンドポイントとコレクションを保護するための複数の手段も提供しています。Faunaを使用してアプリケーションにユーザーを認証する方法については、Faunaでの認証戦略の選択を参照してください。

Before you start

All of the tutorials assume you have already completed the Get started guide, which gets you set up with a Cloudflare Workers account, C3, and Wrangler.

Faunaのセットアップ

データベースを作成する

データベースを作成するには、Fauna Dashboardにログインし、Create Databaseをクリックします。プロンプトが表示されたら、希望するFaunaリージョングループとその他のデータベース設定を選択します。

コレクションを作成する

次のクエリを使用して、データベースにProductsコレクションを作成します。Fauna Dashboardでクエリを実行するには、データベースを選択し、Shellタブをクリックします。

新しいコレクションを作成
Collection.create({ name: "Products" });

クエリは次のような結果を出力します。

出力
{
name: "Products",
coll: Collection,
ts: Time("2099-08-28T15:03:53.773Z"),
history_days: 0,
indexes: {},
constraints: []
}

秘密鍵を作成する

本番環境では、WorkerはCloudflare Fauna統合を使用してFaunaに自動的に接続します。この統合は、Faunaとの認証に必要な資格情報を作成します。

ローカル開発では、手動でFauna認証キーを作成し、キーの秘密を開発秘密としてWorkerに渡す必要があります。

Fauna認証キーを作成するには:

  1. Fauna DashboardのExplorerページの左上のペインで、データベースを選択し、Keysタブをクリックします。

  2. Create Keyをクリックします。

  3. RoleとしてServerを選択します。

  4. Saveをクリックします。

  5. Key Secretをコピーします。この秘密はデータベースにスコープされています。

Workersで在庫を管理する

新しいWorkerプロジェクトを作成する

C3を使用して新しいプロジェクトを作成します。

Terminal window
npm create cloudflare@latest -- fauna-workers

このガイドを続けるには:

  • What would you like to start with? で、Framework Starterを選択します。
  • Which development framework do you want to use? で、Honoを選択します。
  • Do you want to deploy your application? で、Noを選択します。

次に、新しく作成したディレクトリに移動します:

Terminal window
cd fauna-workers

wrangler.tomlファイルを更新して、Workerの名前を設定します。

wrangler.toml
name = "fauna-workers"

ローカル開発用のFaunaデータベースキーを追加する

ローカル開発用に、プロジェクトのルートに.dev.varsファイルを追加し、Faunaキーの秘密を開発秘密として追加します:

.dev.vars
DATABASE_KEY=<FAUNA_SECRET>

Fauna統合を追加する

WorkerをCloudflareにデプロイして、すべてが正しく設定されていることを確認します:

Terminal window
npm run deploy
  1. Cloudflareダッシュボードにログインします。

  2. Integrationsタブを選択し、Fauna統合をクリックします。

    Fauna統合の選択

  3. Faunaアカウントにログインします。

  4. 先ほど作成したFaunaデータベースを選択します。

  5. データベースロールとしてserverロールを選択します。

  6. Secret NameとしてDATABASE_KEYを入力します。

  7. Finishを選択します。

  8. Settingsタブに移動し、Variablesを選択します。新しい変数DATABASE_KEYがWorkerに追加されていることに注意してください。

統合により、新しいFauna認証キーが作成され、キーの秘密がWorkerのDATABASE_KEY秘密に保存されます。デプロイされたWorkerはこのキーを使用します。

依存関係をインストールする

新しく作成したWorkerプロジェクトにFauna JavaScriptドライバーをインストールします。

Faunaドライバーをインストール
npm install fauna

基本的な在庫ロジック

src/index.tsファイルの内容をAPIのスケルトンに置き換えます:

src/index.ts
import { Hono } from "hono";
import { Client, fql, ServiceError } from "fauna";
type Bindings = {
DATABASE_KEY: string;
};
type Variables = {
faunaClient: Client;
};
type Product = {
id: string;
serialNumber: number;
title: string;
weightLbs: number;
quantity: number;
};
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();
app.use("*", async (c, next) => {
const faunaClient = new Client({
secret: c.env.DATABASE_KEY,
});
c.set("faunaClient", faunaClient);
await next();
});
app.get("/", (c) => {
return c.text("Hello World");
});
export default app;

これはFaunaクライアントを初期化し、後で別のハンドラーで使用するためにc.set()でインスタンスを設定するカスタムミドルウェアです:

Faunaクライアントのカスタムミドルウェア
app.use("*", async (c, next) => {
const faunaClient = new Client({
secret: c.env.DATABASE_KEY,
});
c.set("faunaClient", faunaClient);
await next();
});

DATABASE_KEY環境変数にはc.env.DATABASE_KEYからアクセスできます。WorkersはNode.jsの代わりにカスタムJavaScriptランタイムで実行されるため、process.envを使用して環境変数にアクセスすることはできません。

製品ドキュメントを作成する

最初のHonoハンドラーをsrc/index.tsファイルに追加します。このルートは/productsエンドポイントへのPOSTリクエストを受け付けます:

製品ドキュメントを作成
app.post("/products", async (c) => {
const { serialNumber, title, weightLbs } =
await c.req.json<Omit<Product, "id">>();
const query = fql`Products.create({
serialNumber: ${serialNumber},
title: ${title},
weightLbs: ${weightLbs},
quantity: 0
})`;
const result = await c.var.faunaClient.query<Product>(query);
return c.json(result.data);
});

このルートは、Productsコレクションに新しいドキュメントを作成するFQLクエリをfql関数内で適用しました:

JavaScript内のFQLでのクエリ作成
fql`Products.create({
serialNumber: ${serialNumber},
title: ${title},
weightLbs: ${weightLbs},
quantity: 0
})`;

ドキュメントがどのように見えるかを確認するには、次のクエリを実行します。Faunaダッシュボードで、Explorer > リージョン名 > データベース名(例:cloudflare_rest_api) > SHELLウィンドウに移動します:

純粋なFQLでのクエリ作成
Products.create({
serialNumber: "A48432348",
title: "Gaming Console",
weightLbs: 5,
quantity: 0,
});

Faunaは作成されたドキュメントを返します:

新しく作成されたドキュメント
{
id: "<document_id>",
coll: Products,
ts: "<timestamp>",
serialNumber: "A48432348",
title: "Gaming Console",
weightLbs: 5,
quantity: 0
}

作成したルートを調べると、クエリが成功した場合、新しく作成されたドキュメントのデータがレスポンスボディに返されます:

新しいドキュメントデータを返す
return c.json({
productId: result.data,
});

エラーハンドリング

Faunaがエラーを返すと、クライアントによって例外が発生します。この例外をapp.onError()でキャッチし、ServiceErrorのインスタンスから結果を取得して応答できます。

エラーを処理する
app.onError((e, c) => {
if (e instanceof ServiceError) {
return c.json(
{
status: e.httpStatus,
code: e.code,
message: e.message,
},
e.httpStatus,
);
}
console.trace(e);
return c.text("内部サーバーエラー", 500);
});

製品ドキュメントを取得する

次に、Productsコレクションから単一のドキュメントを読み取るルートを作成します。

次のハンドラーをsrc/index.tsファイルに追加します。このルートは/products/:productIdエンドポイントへのGETリクエストを受け付けます:

製品ドキュメントを取得する
app.get("/products/:productId", async (c) => {
const productId = c.req.param("productId");
const query = fql`Products.byId(${productId})`;
const result = await c.var.faunaClient.query<Product>(query);
return c.json(result.data);
});

FQLクエリは、Productionsコレクションから完全なドキュメントを取得するためにbyId()メソッドを使用します:

JavaScript内のFQLでIDによるドキュメントを取得
fql`Products.byId(productId)`;

ドキュメントが存在する場合は、レスポンスボディに返します:

レスポンスボディにドキュメントを返す
return c.json(result.data);

存在しない場合は、エラーが返されます。

製品ドキュメントを削除する

製品ドキュメントを削除するロジックは、製品を取得するロジックと似ています。次のルートをsrc/index.tsファイルに追加します:

製品ドキュメントを削除する
app.delete("/products/:productId", async (c) => {
const productId = c.req.param("productId");
const query = fql`Products.byId(${productId})!.delete()`;
const result = await c.var.faunaClient.query<Product>(query);
return c.json(result.data);
});

前のルートとの唯一の違いは、delete()メソッドをbyId()メソッドと組み合わせてドキュメントを削除することです。

削除操作が成功すると、Faunaは削除されたドキュメントを返し、ルートは削除されたドキュメントをレスポンスのボディに転送します。そうでない場合は、エラーが返されます。

Workerをテストしてデプロイする

Workerをデプロイする前に、Wranglerのdevコマンドを使用してローカルでテストします:

Workerを開発する
npm run dev

開発サーバーが起動したら、WorkerにHTTPリクエストを送信し始めます。

まず、新しい製品を作成します:

新しい製品を作成
curl \
--data '{"serialNumber": "H56N33834", "title": "Bluetooth Headphones", "weightLbs": 0.5}' \
--header 'Content-Type: application/json' \
--request POST \
http://127.0.0.1:8787/products

次のような200レスポンスを受け取るはずです:

製品作成レスポンス
{
"productId": "<document_id>"
}

次に、作成したドキュメントを読み取ります:

ドキュメントを読み取る
curl \
--header 'Content-Type: application/json' \
--request GET \
http://127.0.0.1:8787/products/<document_id>

レスポンスは、新しいドキュメントがJSONにシリアライズされたものになるはずです:

製品読み取りレスポンス
{
"coll": {
"name": "Products"
},
"id": "<document_id>",
"ts": {
"isoString": "<timestamp>"
},
"serialNumber": "H56N33834",
"title": "Bluetooth Headphones",
"weightLbs": 0.5,
"quantity": 0
}

最後に、wrangler deployコマンドを使用してWorkerをデプロイします:

Workerをデプロイする
npm run deploy

これにより、ワーカーが *.workers.dev サブドメインに公開されます。

在庫数量の更新

最後のステップとして、在庫内の商品の数量を更新するルートを実装します。デフォルトでは数量は 0 です。

これには問題が生じます。商品の総数量を計算するには、まず在庫内に現在いくつのアイテムがあるかを確認する必要があります。これを2つのクエリで解決すると、最初に数量を読み取り、その後更新するため、元のデータが変更される可能性があります。

次のルートを src/index.ts ファイルに追加します。このルートは、HTTP PATCH リクエストに /products/:productId/add-quantity URL エンドポイントで応答します:

在庫数量の更新
app.patch("/products/:productId/add-quantity", async (c) => {
const productId = c.req.param("productId");
const { quantity } = await c.req.json<Pick<Product, "quantity">>();
const query = fql`Products.byId(${productId}){ quantity : .quantity + ${quantity}}`;
const result =
await c.var.faunaClient.query<Pick<Product, "quantity">>(query);
return c.json(result.data);
});

FQLクエリをより詳細に確認します:

JavaScript内のFQLでの更新クエリ
fql`Products.byId(${productId}){ quantity : .quantity + ${quantity}}`;

更新ルートをテストします:

商品在庫の更新
curl \
--data '{"quantity": 5}' \
--header 'Content-Type: application/json' \
--request PATCH \
http://127.0.0.1:8787/products/<document_id>/add-quantity

レスポンスは、数量に5つの追加アイテムが含まれた更新されたドキュメント全体であるべきです:

商品更新のレスポンス
{
"quantity": 5
}

ワーカーをCloudflareにデプロイして更新します。

Cloudflareでワーカーを更新する
npm run deploy