🌟

Cloudflare Workers + Hono + Prismaでローカル環境構築

2024/05/15に公開

概要

タイトルの通りですが、Cloudflare Workers + Hono + Prismaでローカル環境構築を行ってみたいと思います。

Hono

https://hono.dev/

Edgesのための小さくシンプルで超高速なWebフレームワークです。どのJavaScriptランタイムでも動作します:Cloudflare Workers、Fastly Compute、Deno、Bun、Vercel、Netlify、AWS Lambda、Lambda@Edge、Node.js

Prisma

https://www.prisma.io/

Prismaは、Node.jsとTypeScriptのための次世代のORM (Object-Relational Mapping) です。データベースとのやり取りをより簡単かつ安全にするために設計されています。開発者がデータベーススキーマを定義し、そのスキーマに基づいて自動的に型安全なクライアントAPIを生成します。これにより、SQLやデータベース固有のクエリ言語に依存することなく、データベースとのやり取りが可能になります。

環境構築

ローカルでの開発環境ですが、事前に作成した以下のリポジトリの環境をベースに進めていきたいと思います。

https://github.com/Slowhand0309/nodejs-devcontainer-boilerplate

開発環境としてはIDE VSCodeDevContainer を使用して開発する前提で進めていきます。

プロジェクト名は hono-prisma-workers-example として↑のリポジトリから「Use this template」で新規に作成して進めていきます。

次に .devcontainer/Dockerfile に以下を追加します。

FROM node:20.10.0-bullseye-slim
LABEL maintainer="Slowhand0309"

ARG username=vscode
ARG useruid=1000
ARG usergid=${useruid}

# create hono時のgit cloneでca証明書が必要なので ca-certificates を追加
# パッケージ管理として pnpm を使用する為追加
RUN set -ex \
    && apt-get update \
    && apt-get install -y \
        sudo \
        ca-certificates \
        --no-install-recommends \
    # Delete node user with uid=1000 and create vscode user with uid=1000
    && userdel -r node \
    && groupadd --gid ${usergid} ${username} \
    && useradd -s /bin/bash --uid ${useruid} --gid ${usergid} -m ${username} \
    && echo ${username} ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/${username} \
    && chmod 0440 /etc/sudoers.d/${username} \
    && npm install -g pnpm
    && rm -rf /var/lib/apt/lists/*

USER ${username}

次に .devcontainer/docker-compose.ymlports を追加しときます。

services:
  app:
    ...
    ports:
      - "8787:8787" # 追加

一旦ここまででVSCode上で Command + Shift + P から「Dev Container: Reopen」を選択し、DevContainerを起動します。

早速pnpm create hono@latest . で新規プロジェクトを作成します。

templateには cloudflare-workers を選択します。

create-hono version 0.6.3
✔ Using target directory … .
? Which template do you want to use? cloudflare-workers
? Directory not empty. Continue? yes
✔ Cloning the template
? Do you want to install project dependencies? yes
? Which package manager do you want to use? pnpm
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd .

wrangler.tomlname にプロジェクト名を設定しコンテナ外からもアクセスできるように以下を追加しときます。

[dev]
ip = "0.0.0.0"
port = 8787

これで pnpm dev を実施し [http://localhost:8787](http://localhost:8787) にアクセスし Hello Hono! が表示されていればOKです。

毎回 Would you like to help improve Wrangler by sending usage metrics to Cloudflare? と聞かれるのを回避する(不必要であればスキップしてください)

Containerを起動し直す度に↑こちら聞かれるので、初回の1回のみで次からは聞かれないようにしてみたいと思います。

初回聞かれた際に↑の情報は ~/.config/.wrangler/metrics.json に保存されます。そこで ~/.config/.wranglervolume に bind します。

  • .devcontainer/docker-compose.yml に以下を追加します

    version: "3.8"
    volumes:
      modules_data:
      wrangler_data: # 追加
    
    services:
      app:
        build: .
        image: slowhand/nodejs
        container_name: "hono-prisma-workers"
        volumes:
          - ..:/usr/src
          - modules_data:/usr/src/node_modules
          - wrangler_data:/home/vscode/.config/.wrangler # 追加
        command: /bin/sh -c "while sleep 1000; do :; done"
        working_dir: /usr/src
        ports:
          - "8787:8787"
          - "5555:5555"
    
    
  • .devcontainer/postAttach.sh に以下を追加します

    sudo chown -R vscode /home/vscode/.config
    

これでコンテナ起動しても聞かれるのは初回のみで、次回からは聞かれなくなります。

PostgreSQLを追加

.devcontainer/docker-compose.yml に以下を追加します。

volumes:
  db_data:
  
services:
  db:
    image: postgres:16.2
    container_name: postgres_pta
    ports:
      - 5432:5432
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres

Prismaの設定

コンテナを起動し直して、Prismaのインストール & 設定をやっていきます。

pnpm add -D prisma
pnpm prisma init --datasource-provider postgresql

生成された .env ファイルの DATABASE_URL を以下に修正します。

DATABASE_URL="postgresql://postgres:postgres@db:5432/example?schema=public"

次に prisma/schema.prismaUser テーブルを追加します。

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
}

ここまででマイグレーションを実行してみます。

$ pnpm prisma migrate dev --name init
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "example", schema "public" at "db:5432"

PostgreSQL database example created at db:5432

Applying migration `20240417131428_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20240417131428_init/
    └─ migration.sql

Your database is now in sync with your schema.

Running generate... (Use --skip-generate to skip the generators)
 WARN  2 deprecated subdependencies found: rollup-plugin-inject@3.0.2, sourcemap-codec@1.4.8
Packages: +1
+
Progress: resolved 123, reused 96, downloaded 1, added 1, done
node_modules/.pnpm/@prisma+client@5.12.1_prisma@5.12.1/node_modules/@prisma/client: Runninnode_modules/.pnpm/@prisma+client@5.12.1_prisma@5.12.1/node_modules/@prisma/client: Running postinstall script, done in 18ms

dependencies:
+ @prisma/client 5.12.1

Done in 5s

✔ Generated Prisma Client (v5.12.1) to ./node_modules/.pnpm/@prisma+client@5.12.1_prisma@5
.12.1/node_modules/@prisma/client in 15ms

これで、実際に User テーブルが作成され、 @prisma/client パッケージが追加し内部的に prisma generate が実行されます。

DBのテーブルやデータの確認には別途クライアントツールを使ったり、Prismaが提供しているブラウザで確認できる Prisma Studio も使えます。

pnpm prisma studio

↑をコンテナ内で実行し、http://localhost:5555 にアクセスすると User テーブルが作成されているのが分かるかと思います。

image1.png

Seedデータ登録

まずは必要なパッケージをインストールします。

 pnpm add -D ts-node @types/node

次に tsconfig.json を修正します。

{
  "compilerOptions": {
    "target": "es2016", // 修正
    "module": "commonjs", // 修正
    "strict": true,
    "lib": ["ESNext"],
    "types": ["node", "@cloudflare/workers-types"], // node追加
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  }
}

次に prisma/seed.ts を以下内容で作成します。

import { Prisma, PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

const userData: Prisma.UserCreateInput[] = [...Array(5)].map((_, i) => ({
  name: `User${i + 1}`,
  email: `user${i + 1}@example.com`,
}));

const main = async () => {
  const result = await prisma.user.createMany({
    data: userData,
    skipDuplicates: true,
  });
  console.log({ result });
};

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

package.json に以下を追加します。

  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  }

ここまできたら実際にseedデータを登録してみます。

$ pnpm prisma db seed
Environment variables loaded from .env
Running seed command `ts-node prisma/seed.ts` ...
{ result: { count: 5 } }

🌱  The seed command has been executed.

Prisma Studio で確認してデータ登録できとけばOKです。

image2.png

API実装

いよいよAPI実装ですが、Cloudflare WorkersでPrismaを使用する場合、PrismaのEdge機能を利用する事になるのですが、Engine 部分が Cloudflare Workers上で動作しない為リモート上に別途用意し、そこ経由で接続する必要があります。

詳しくは こちら

Prisma Accelerate などの外部サービスを使って接続可能です。が今回はローカルで動作させたい為、外部サービスに接続して〜なんてことはしたくありません。

※ ちなみにそのまま Cloudflare Workers上でPrismaを使おうとすると次のエラーが出ます。

[ERROR] Error: PrismaClient is not configured to run in Cloudflare Workers. In order to run Prisma Client on edge runtime, either:

  - Use Prisma Accelerate: https://pris.ly/d/accelerate
  - Use Driver Adapters: https://pris.ly/d/driver-adapters

メッセージにもある様に Driver Adapters を使って試してみたいと思います。

https://next-blog.croud.jp/contents/d618519a-fc88-4eb0-833e-deb2fbd662ea

[node-postgres](https://node-postgres.com/) を使う

Cloudflare のconnect()(TCP) を使用してデータベースにアクセスします。 Cloudflare Workers とのみ互換性があり、Vercel Edge Functions とは互換性がありません。

  • prisma/schema.prisma に以下を追加
generator client {
  provider = "prisma-client-js"
  previewFeatures = ["driverAdapters"] # ← 追加
}
  • PrismaClientを再生成
pnpm prisma generate
  • pgパッケージと Prisma の Driver Adapter をインストール
pnpm add pg @prisma/adapter-pg
pnpm add -D @types/pg
  • wrangler.tomlnode_compat = true を追加する
name = "hono-prisma-workers-example"
compatibility_date = "2023-12-01"
node_compat = true # 追加
  • .dev.vars を以下内容で作成する
DATABASE_URL="postgresql://postgres:postgres@db:5432/example?schema=public"
  • APIリクエスト部分の作成
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "@prisma/client";
import { Hono } from "hono";
import { Pool } from "pg";

const app = new Hono();

app.get("/", (c) => {
  return c.text("Hello Hono!");
});

app.get("/users", async (c) => {
  const connectionString = `${c.env?.DATABASE_URL ?? ""}`;
  const pool = new Pool({ connectionString });
  const adapter = new PrismaPg(pool);
  const prisma = new PrismaClient({ adapter });
  const users = await prisma.user.findMany();
  return c.json(users);
});

export default app;

ここまで出来たら http://localhost:8787/users にアクセスし、以下の様なレスポンスが返ってきたらOKです。

[
  {
    "id": 1,
    "email": "user1@example.com",
    "name": "User1"
  },
  {
    "id": 2,
    "email": "user2@example.com",
    "name": "User2"
  },
  {
    "id": 3,
    "email": "user3@example.com",
    "name": "User3"
  },
  {
    "id": 4,
    "email": "user4@example.com",
    "name": "User4"
  },
  {
    "id": 5,
    "email": "user5@example.com",
    "name": "User5"
  }
]

トラブルシューティング

pnpm create hono 時に以下エラーが発生する。

 throw new DegitError(`could not find commit hash for ${repo.ref}`, {

git がインストールされていない場合インストールして再度実行する。

https://zenn.dev/aipacommander/scraps/a1b42841d22bf1

git がインストールされている場合、 git ls-remote https://github.com/honojs/starter を実行してみて以下のエラーが発生する場合、

fatal: unable to access 'https://github.com/honojs/starter': server certificate verification failed. CAfile: none CRLfile: none

SSLの証明証がインストールされていないので、インストールして再度実行する。

sudo apt-get install --reinstall ca-certificates

https://stackoverflow.com/questions/35821245/github-server-certificate-verification-failed

https://github.com/tiged/tiged/blob/1d5587d8bd26ce999fc3132d0fe839f42cd3d967/src/index.js#L286


APIリクエスト時に以下エラーが発生する

[ERROR] A hanging Promise was canceled. This happens when the worker runtime is waiting for a Promise from JavaScript to resolve, but has detected that the Promise cannot possibly ever resolve because all code and events related to the Promise's I/O context have already finished.

src/index.ts にて app.get 外で PrismaClient 定義した場合に発生したエラーになります。

const connectionString = `${c.env?.DATABASE_URL ?? ""}`;
const pool = new Pool({ connectionString });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
const app = new Hono();

app.get("/users", async (c) => {
  const users = await prisma.user.findMany();
  return c.json(users);
});

きちんとコネクションが終わらず残ってしまっている状態になっているのかもしれません。もしかしたら他に解決策があるかもですが、今回は app.get 内に PrismaClinet 定義を移動させて解放してやるようにしました。

リポジトリ

今回の内容は以下のリポジトリにpushしてます。

https://github.com/Slowhand0309/hono-prisma-workers-example

参考URL

Discussion