Cloudflare Workers + Hono + Cloudflare D1を使って、CDNエッジのみで動くWebAPIを作ってみた

2022.12.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

遅くなりました。本記事はCloudflareアドベントカレンダーの16日目の記事です。

Cloudflare Advent Calendar 2022 の記事一覧

この記事では、Cloudflare WorkersとCloudflare D1を使って、CDNエッジ上で動作するWebAPIを作成してみたいと思います

Cloudflare D1で何が変わったか

もともと、Cloudflare Workersは既存のアプリケーションの補助的に使われることが多いサービスの印象でした。例えば、リクエスト時のA/Bテスト振り分けとかHTTP Headerの追加、変更などです。CDN上で何らかの処理を施してオリジンのアプリケーションに送るようなプロキシ的な動作をさせるのが一般的だったと思います。

D1の目標は、電子商取引サイト、会計ソフト、SaaSソリューション、CRMなど、APIからリッチでパワフルなアプリケーションまで、あらゆるものの構築に貢献することです。

引用: https://blog.cloudflare.com/ja-jp/introducing-d1-ja-jp/

とあるように、

Cloudflare D1が登場したことで、リレーショナルデータベースを使った幅広いユースケースに対応したアプリケーションを作成できるようになりました。また、これらがすべてCDN上で動作することになります。Cloudflare Workers専用のDBなので、実装する際は以下のサービスを組み合わせて開発していくことになります。

  • Cloudflare Pages: SPAのホスティング(not SSR)
  • Cloudflare Workers: Functionサービス(ビジネスロジック、API、SSRなど)
  • Cloudflare R2: 画像などの静的ファイルの保存、S3互換
  • Cloudflare D1: sqliteデータベース
  • Cloudflare KV: キーバリューストア
  • Cloudflare Queue: キュー

この記事では、Cloudflare Workersとその上で動くWebフレームワークのHono、Cloudflare D1を使ってWebAPIを作ってみたいと思います。

2022/12月現在、Cloudflare D1はAlpha版のため、Productionでの利用は控えたほうが良いです。

Environment

MacBook Pro (14-inch, M1 Pro, 2021)
OS : macOS Monterey 12.5

node --version
v18.12.1

npm --version
8.19.2

また事前に、Cloudflare のサイトからサインアップしてCloudflare Workersのダッシュボードにアクセスできるようにしておきます。

Getting Started

Wranglarのインストールとログイン

適当な名前でフォルダを作成し、WranglarというCloudflare Workersの開発ツールをインストールしログインします。また、Wranglerの初期化をします。

npm install -g wrangler
npx wrangler login
mkdir cloudflare-advent-calendar-16
cd cloudflare-advent-calendar-16
npx wrangler init -y

Honoのインストール

Cloudflare Workersで動くWebフレームワークのHonoをインストールします。

npm install hono

miniflareのインストール

d1を使うので、Cloudflare Workersのエミュレート環境の miniflare をインストールします。これでローカルでD1の動作確認ができます。miniflare自体はwranglerで --local オプションを使うことで自動的に使用されるようです。

npm install --save-dev miniflare

Hello World!

Expressを使ったことがある方は馴染みがあるかと思いますが、Honoをインポートしてnewしてルーティングを定義するだけです。最小構成だと4行でHelloWorldのAPIを作成することができます。

import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => c.text("Hello World!!!"));

export default app;

localhostで起動

localhostで起動することを確認します。

npx wrangler dev

Cloudflare Workers にデプロイ

エントリーポイントとなる index.ts を指定して、Cloudflare Workersにデプロイします。デプロイ後、Cloudflareのダッシュボードからアクセスして、ブラウザに Hello World!! が表示されればOKです。

npx wrangler publish ./src/index.ts

Cloudflare D1のデータベースを作成する

データベースの作成は先程インストールしたwranglerで行うことができます。 qiita-advent-16 というデータベースを作成します。作成するとプロジェクト直下の .wrangler/stage/d1 フォルダにsqliteファイルが作成されていると思います。これがローカルで使用されるものです。

npx wrangler d1 create qiita-advent-16

D1 バインディングを作成

次にCloudflare WorkersからD1を使えるようにするために、D1のバインディングを作成します。バインディングとは、WorkersからD1、R2、Queue、StreamなどのCloudflareリソースにアクセスするための設定のようなものです。設定は、プロジェクト直下の wrangler.toml に追記します。ここに追記することで複数のリソースと接続することができます。設定は上のコマンド実行後に出力されるのでそれをコピーします。

[[ d1_databases ]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "qiita-advent-16"
database_id = "hogehoge"

また、Envの型を定義することでタイプセーフで実装することができます。その際は以下のCloudflare Workersの型情報をインストールし、interfaceとして定義しHonoに渡します。

npm install --save-dev @cloudflare/workers-types
export interface Env {
  DB: D1Database;
}

const app = new Hono<{Bindings: Env}>();

テーブルを作成する

D1が使えるようになったので、テーブルを2つ作成してJOINするようなサンプルにしたいと思います。今回はユーザーを管理するテーブルを作成したいと思います。要件は以下です。

  • ユーザーは1つのグループに所属することができる

wranglerでSQLを実行できるので、テーブルを作成します。 schema.sql ファイルを作成します。

DROP TABLE IF EXISTS users;
CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT, 
    username TEXT NOT NULL,
    email TEXT NOT NULL,
    group_id INT,
    created_at TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')),
    updated_at TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime'))
);

DROP TABLE IF EXISTS groups;
CREATE TABLE groups (
    id INTEGER PRIMARY KEY AUTOINCREMENT, 
    groupname TEXT NOT NULL,
    created_at TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime')),
    updated_at TEXT NOT NULL DEFAULT (DATETIME('now', 'localtime'))
);

作成できたら、wrangler を使ってテーブルを作成します。 --local をつけてまずはminiflareのエミュレート環境のD1に作成します。

npx wrangler d1 execute qiita-advent-16 --local --file=./schema.sql

テーブルが作成されていることが確認できました。ここまでではまだローカルでテーブルを作っただけなので、リモートのD1には反映されていません。

npx wrangler d1 execute qiita-advent-16 --command="select name from sqlite_master where type='table'"
┌─────────────────┐
│ name            │
├─────────────────┤
│ d1_kv           │
├─────────────────┤
│ users           │
├─────────────────┤
│ sqlite_sequence │
├─────────────────┤
│ groups          │
└─────────────────┘

D1 用のクエリビルダーを使ってアクセスしてみる

既存のNode.js用ORMについて

D1の実態はsqliteですが、sqlite対応のTypeORMやPrismaといったORMは現状使うことができません。Prisma のGitHubを見てみるとIssueは上がっているので、もしかしたら対応されるかもしれません。

https://github.com/prisma/prisma/issues/13310

D1用クエリビルダを使ってアクセスする

代わりにD1 Client APIというものががあるのでこちらを使用してアクセスします。D1バインディングを作成した場合に自動で構成されるみたいです。D1 Client APIを使ってもいいんですがそれをラップして使いやすくした workers-qb のようなD1専用のクエリビルダーライブラリがあります。今回はこのライブラリを使ってデータベースにアクセスしてみようと思います。

まず、workers-qb をインストールします。

npm install workers-qb

index.ts を以下のように修正します。 workeres-qb をインポートして、D1のバインディングを引数に渡します。ユーザー作成APIとユーザー取得APIを作成します。

import { Hono } from "hono";
import { D1QB } from "workers-qb";

export interface Env {
  DB: D1Database;
}

const qb = new D1QB(env.DB);
const app = new Hono();

app.get("/", (c) => c.text("Hello World!!!"));

interface CreateUserBody {
  username: string;
  email: string;
  groupId: string;
}

app.post('/api/users', async c => {
  try {
    console.log('c', c);

    const body = await c.req.json<CreateUserBody>();

    const qb = new D1QB(c.env.DB);
    const inserted = await qb.insert({
      tableName: 'users',
      data: {
        username: body.username,
        email: body.email,
        group_id: body.groupId,
      },
    });
    return c.json(inserted);
  } catch (e) {
    console.error(e);
    throw e;
  }
});

app.get('/api/users/:id', async c => {
  console.log('c', c);

  const {id} = c.req.param();
  const qb = new D1QB(c.env.DB);
  const fetched = await qb.execute({
    query:
      'SELECT users.id, users.username, users.email, groups.groupname FROM users JOIN groups ON users.id = groups.id WHERE users.id = ?1',
    arguments: [id],
    fetchType: FetchTypes.ONE,
  });

  console.log('fetched', fetched);
  return c.json(fetched);
});

export default app;

APIを書けたら、ローカルのD1向けに動作確認してみます。以下のように実行することでローカルのd1に自動的に接続してくれます。

npx wrangler dev --local --persist

ユーザー作成APIを実行します。

curl -POST -H"Content-Type: application/json" "http://localhost:8787/api/users" -d'{
  "username": "sato.naoya",
  "email": "sato.naoya@classmethod.jp",
    "groupId": 1,
}'

エミュレート環境のD1に保存されていることを確認します。

npx wrangler d1 execute qiita-advent-16 --local --command='SELECT * FROM users'
┌────┬────────────┬───────────────────────────┬──────────┬─────────────────────┬─────────────────────┐
│ id │ username   │ email                     │ group_id │ created_at          │ updated_at          │
├────┼────────────┼───────────────────────────┼──────────┼─────────────────────┼─────────────────────┤
│ 1  │ sato.naoya │ sato.naoya@classmethod.jp │ 1        │ 2022-12-22 13:12:54 │ 2022-12-22 13:12:54 │
└────┴────────────┴───────────────────────────┴──────────┴─────────────────────┴─────────────────────┘

ユーザー取得APIを実行します。 users テーブルと groups テーブルをJOINした結果を取得できました。

curl -H"Content-Type: application/json" "http://localhost:8787/api/users/1"
{"id":1,"username":"sato.naoya","email":"sato.naoya@classmethod.jp","groupname":"test-group"}⏎

試しに —local オプションをなくすと、リモートのCloudflare D1 には値が保存されていないことも確認できました。

npx wrangler d1 execute qiita-advent-16 --command='SELECT * FROM users'

ローカルのデータベースの変更をCloudflare D1に反映する

ローカルで動作確認ができたら、Cloudflare D1に変更を反映します。先程のcliから --local オプションをなくすことで反映させることができます。

npx wrangler d1 execute qiita-advent-16 --file=./schema.sql

Cloudflare Workersにデプロイ

修正したアプリケーションをデプロイします。

npx wrangler publish ./src/index.ts

これで、ローカルで動作確認したものと同じ内容がCloudflare WorkersとCloudflare D1にデプロイされました。

リポジトリ

今回検証したリポジトリはこちらに置いてあります。

https://github.com/briete/cloudflare-workers-sample

まとめ

オープンAlphaになったため、Cloudflare D1を触ってみました。現状はデータベースあたり100MB制限があったり、トランザクションを使えないなどの制限はありますが、まだAlpha版なので気長に待ちたいと思います。Wranglerの開発体験が良く、miniflareと組み合わせることでシームレスにローカル開発とCloudflareへのデプロイができます。他にもCloudflare Accessを使った認証や、Cloudflare Queueを使ったイベントドリブンなアーキテクチャ、サービスバインディングを使ったマイクロサービスの構築など触ってみたいサービスがありますので、検証してみたいと思いました。