Top View


Author Daiki Urata

Hono + Prisma + Cloudflare D1でさくっとAPIを作ってみる

2024/04/12

はじめに

先日PrismaのCloudflare D1サポートが発表されたので早速使ってみます。 ただ動かすだけではつまらないので、実践的にHonoと組み合わせて簡単なAPIを作っていきます。

環境

  • Node.js v20.12.0
  • pnpm v8.15.6
  • Hono v4.2.3
  • Prisma v5.12.1

Honoのプロジェクト作成

まずはHonoのプロジェクトを作成するところから始めます。 テンプレートをどれにするか聞かれるので、cloudflare-workers を選びます。

pnpm create hono myapp
cd myapp
pnpm dev

これで無事に起動できればOKです。

Cloudflare Workersへのデプロイ

デプロイも簡単で、プロジェクト作成時にすでにwrangler.tomlが置かれているので、コマンド1つでデプロイできます。

pnpm run deploy 

デプロイ後に発行されるURLへアクセスして「Hello Hono!」と表示できれば成功です。

Prismaの導入

ここからが本題でPrismaを導入してD1と連携します。 まずはパッケージのインストールです。

pnpm add -D prisma
pnpm add @prisma/client @prisma/adapter-d1

インストールが終わったら、まずは初期化します。

npx prisma init --datasource-provider sqlite  

すると、Prismaのスキーマファイルが生成されます。 今回はPreview機能を使用するので previewFeatures = ["driverAdapters"] を追記する必要がありました。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
  previewFeatures = ["driverAdapters"] // <- 追加
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

データベース作成

Prismaのセットアップが完了したら、次はいよいよD1のデータベースを用意します。

npx wrangler d1 create myapp-db

⛅️ wrangler 3.49.0
-------------------
 Successfully created DB 'myapp-db' in region APAC
Created your new D1 database.

[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "myapp-db"
database_id = "xxxx-xxxx-xxxx"

ここで出力された通りwrangler.tomlへデータベース設定情報を追記します。

name = "myapp"
compatibility_date = "2023-12-01"
compatibility_flags = ["nodejs_compat"]

[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "myapp-db"
database_id = "xxxx-xxxx-xxxx"

ここで compatibility_flags = ["nodejs_compat"] を追加するのを忘れないでください。 これはWorkerでNode.jsのAPIを有効化するための設定のため必要となります。

マイグレーション

次にマイグレーションを実行してデータベースへテーブルを作成します。 今回はシンプルに都道府県データを返すAPIにするので、そのテーブルを作成します。

npx wrangler d1 migrations create myapp-db create_prefecture_table

コマンドを実行すると以下のような.sqlファイルが作成されます。中身はまだ空です。

migrations/
	└── 0001_create_prefecture_table.sql

次にschema.prismaを編集します。

model Prefecture {
  id    Int     @id @default(autoincrement())
  prefectureId String  @unique
  name  String
}

そして、Prismaのmigrateコマンドを実行します。

npx prisma migrate diff --from-empty --to-schema-datamodel ./prisma/schema.prisma --script --output migrations/0001_create_p
refecture_table.sql

すると先ほどの0001_create_prefecture_table.sqlには

-- CreateTable
CREATE TABLE "Prefecture" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "prefectureId" TEXT NOT NULL,
    "name" TEXT NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "Prefecture_prefectureId_key" ON "Prefecture"("prefectureId");

のような形で書き込まれているはずです。

最後にマイグレーションを実行してデータベースへテーブルを作成します。 まずはローカルに対して実行して確認します。

npx wrangler d1 migrations apply myapp-db --local

問題なければリモート環境へ適用します。

npx wrangler d1 migrations apply myapp-db --remote

これでマイグレーションの完了です。

シード

今回は都道府県データを返すAPIにしたいので、作成したテーブルに対してデータを流し込みたいです。 スマートなやり方がわからず、Prismaのブログではwranglerコマンドを使ってINSERTしていたので、同じようにwraglerを実行する簡単なスクリプトを用意しました。

仮でseed.jsとしておきましょう。

const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);

const prefectures = [
  {
    prefectureId: '01',
    name: '北海道'
  },
  {
    prefectureId: '02',
    name: '青森県'
  },
  // 省略
  {
    prefectureId: '47',
    name: '沖縄県'
  }
]


async function run () {
  const promises = prefectures.map(async prefecture => {
    try {
      await exec(`npx wrangler d1 execute myapp-db --command "INSERT INTO  \"Prefecture\" (\"prefectureId\", \"name\") VALUES  ('${prefecture.prefectureId}', '${prefecture.name}');" --remote`);
    } catch (error) {
      console.error(error);
    }
  })
  await Promise.all(promises);
}

run();

これを実行してデータを用意します。 いきなりリモート環境へ実行するのが不安な人は上のスクリプトの --remote--local へ変更してください。

node seed.js

API作成

さてこれで準備は全て整いました。あとはDBへアクセスしてデータを取得、レスポンスを返す処理を書くだけです。

src/index.tsは以下のようになりました。

import { Hono } from 'hono'
import { PrismaClient } from '@prisma/client'
import { PrismaD1 } from '@prisma/adapter-d1'

type Bindings = {
  DB: D1Database
}

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

app.get('/', async (c) => {
  const adapter = new PrismaD1(c.env.DB)
  const prisma = new PrismaClient({ adapter })

  const prefectures = await prisma.prefecture.findMany()
  return c.json(prefectures)
})

export default app

かなりシンプルですね。 動かしてみる前に、Prisma Client生成のためのコマンドを実行します。

npx prisma generate

そしてローカルで実行確認します。

pnpm dev

ローカルで都道府県データが返ってくるのが確認できたらあとはデプロイするだけです。

pnpm run deploy

ここで問題発生

ローカルでちゃんと動いてデプロイしようと思ったら以下のようなエラーに遭遇しました。


 [ERROR] Could not resolve "util"

    node_modules/.pnpm/@prisma+debug@5.12.1/node_modules/@prisma/debug/dist/index.js:151:29:
      151         const util = require(`${"util"}`);
                              ^
                              "./util"

  The package "util" wasn't found on the file system but is built into node.
  Add "node_compat = true" to your wrangler.toml file and make sure to prefix the module name with "node:" to enable Node.js compatibility.

Cloudflare Workersで compatibility_flags = ["nodejs_compat"] を指定して動かす際にNode.js APIでは node: のプレフィックスをつけてimportしなければならないのですが、 Prisma側(5.12.1時点)でついていない箇所がありエラーになりました。

これをパッチを当てて解決する必要があります。 以下コマンドを実行します。

pnpm patch @prisma/debug

You can now edit the following folder: /private/var/folders/xxxx/xxxx
Once you're done with your changes, run "pnpm patch-commit '/private/var/folders/xxxx/xxxxx

出力されたパスへ行き、該当箇所のdist/index.jsを開いて編集します。 今回で言うと151行目の

const util = require(`${"util"}`);

const util = require(`${"node:util"}`);

に変更しました。

そしてパッチを適用させるコマンドを実行します。

pnpm patch-commit '/private/var/folders/xxxx/xxxx'

すると、patchesディレクトリができ、package.jsonには

"pnpm": {
    "patchedDependencies": {
      "@prisma/debug@5.12.1": "patches/@prisma__debug@5.12.1.patch"
    }
  }

という設定が追加されました。

これで再度デプロイしてみます。

pnpm run deploy

無事にデプロイできました!

おわりに

まだPrismaのD1対応はPreviewなため、色々と問題は出てきそうですが実際に動くようになったのでDrizzle以外の選択肢が広がったのは良いことですね!

参考

Daiki Urata

Daiki Urata

Twitter X

フロントエンド/モバイルアプリなどを主に開発しています。