Faunaを使用してサーバーレスでグローバルに分散したREST APIを作成する
このチュートリアルでは、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での認証戦略の選択 ↗を参照してください。
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 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認証キーを作成するには:
-
Fauna DashboardのExplorerページの左上のペインで、データベースを選択し、Keysタブをクリックします。
-
Create Keyをクリックします。
-
RoleとしてServerを選択します。
-
Saveをクリックします。
-
Key Secretをコピーします。この秘密はデータベースにスコープされています。
C3 ↗を使用して新しいプロジェクトを作成します。
npm create cloudflare@latest -- fauna-workersyarn create cloudflare@latest fauna-workerspnpm 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を選択します。
次に、新しく作成したディレクトリに移動します:
cd fauna-workerswrangler.tomlファイルを更新して、Workerの名前を設定します。
name = "fauna-workers"ローカル開発用に、プロジェクトのルートに.dev.varsファイルを追加し、Faunaキーの秘密を開発秘密として追加します:
DATABASE_KEY=<FAUNA_SECRET>WorkerをCloudflareにデプロイして、すべてが正しく設定されていることを確認します:
npm run deploy-
Cloudflareダッシュボード ↗にログインします。
-
Integrationsタブを選択し、Fauna統合をクリックします。

-
Faunaアカウントにログインします。
-
先ほど作成したFaunaデータベースを選択します。
-
データベースロールとして
serverロールを選択します。 -
Secret Nameとして
DATABASE_KEYを入力します。 -
Finishを選択します。
-
Settingsタブに移動し、Variablesを選択します。新しい変数
DATABASE_KEYがWorkerに追加されていることに注意してください。
統合により、新しいFauna認証キーが作成され、キーの秘密がWorkerのDATABASE_KEY秘密に保存されます。デプロイされたWorkerはこのキーを使用します。
新しく作成したWorkerプロジェクトにFauna JavaScriptドライバー ↗をインストールします。
npm install faunayarn add faunasrc/index.tsファイルの内容をAPIのスケルトンに置き換えます:
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()でインスタンスを設定するカスタムミドルウェアです:
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関数内で適用しました:
fql`Products.create({ serialNumber: ${serialNumber}, title: ${title}, weightLbs: ${weightLbs}, quantity: 0})`;ドキュメントがどのように見えるかを確認するには、次のクエリを実行します。Faunaダッシュボードで、Explorer > リージョン名 > データベース名(例:cloudflare_rest_api) > SHELLウィンドウに移動します:
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() ↗メソッドを使用します:
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をデプロイする前に、Wranglerのdevコマンドを使用してローカルでテストします:
npm run devyarn 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をデプロイします:
npm run deployyarn 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クエリをより詳細に確認します:
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にデプロイして更新します。
npm run deployyarn deploy