React Router × Cloudflare Workers × Supabase で Drizzle ORM を試す


こんにちは。Gaji-Labo の村上です。
React Router × Cloudflare Workers × Supabase で個人開発を進めるなか、ORM を導入したいと考えました。
そこで、Drizzle ORM を試してみたところ、相性が良さそうに感じたため紹介します!

この構成は、なるべくコストをかけずに開発したい方におすすめです!
Cloudflare Workers は無料枠があり、Supabase も無料プランで十分な機能を提供しているため、コストを抑えながらアプリを構築できます。

本記事では、Drizzle ORM の導入をメインに、ローカル環境を構築し初期データ (seed) を Supabase に投入するところまで紹介します。

React Router:ver 7.1.5
Supabase CLI:ver 2.9.6
Drizzle ORM:ver 0.39.1

相性がいいと感じたポイント

Supabase とのマイグレーション管理がスムーズ

Supabase CLI にはスキーマ管理の仕組みがないため、通常は SQL ファイルで手動管理する必要があります。
しかし、Drizzle ORM を使えば、スキーマを TypeScript で定義し、マイグレーションファイルを Drizzle で生成 → Supabase CLI で適用 という流れで管理できます。

Cloudflare Workers でも動作する

Cloudflare Workers 環境で動作しない ORM もありますが、Drizzle ORM は Cloudflare Workers でも動作 します。
また、Drizzle はサイズが軽量で Prisma 導入時よりも早く動作するようです。
参考:https://zenn.dev/saneatsu/scraps/7be2fcc923be42

RLS に対応している

Prisma では公式にサポートされていないですが、Drizzle は公式にサポートされていて便利です。
参考:https://orm.drizzle.team/docs/rls

最終的なディレクトリ構成

/my-project
├── app
│   ├── db
│   │   ├── client.server.ts
│   │   ├── schema.ts
│   │   └── seeds.ts
│   ├── routes
│   │   ├── api.users.get.tsx
│   │   └── home.tsx
│   ├── ...
│   └── routes.ts
├── public
├── supabase
├── workers
├── .dev.vars
├── .gitignore
├── drizzle.config.ts
├── package.lock.json
├── package.json
├── ...
├── vite.config.ts
├── worker-configuration.d.ts
└── wrangler.toml

React Router プロジェクト作成

React Router の公式ドキュメントに沿ってプロジェクトを作成していきます。
今回は cloudflare テンプレートを利用します。

npx create-react-router@latest --template remix-run/react-router-templates/cloudflare

Supabase CLI を導入

Supabase Local Development & CLIに則って進めます。
npx supabase start 実行後にターミナルに表示される、Supabase の DB URL は後ほど使うのでコピーしておきます。

Drizzle ORM を導入

関連パッケージをインストールし、初期データ投入まで行います。

関連パッケージをインストール

npm i drizzle-orm postgres drizzle-seed dotenv
npm i -D drizzle-kit dotenv-cli tsx pg

環境変数を用意

wrangler を使って開発をしているので、ローカルの環境変数を .dev.vars ファイルを作成して定義します。
.dev.vars ファイル作成後は、.gitignore に追加しておきます。

// supabase の DB URL 
DATABASE_URL="postgresql:xxxxxxxxx"

次に、package.json の scriptsに"typegen": "wrangler types" scripts を追加します。

npm run typegen を実行すると .dev.vars から型定義ファイル(worker-configuration.d.ts)を生成され、DATABASE_URL の型定義が記載されているのが確認できます。

テーブルスキーマを定義

app/db/schema.ts ファイルを作成し、users テーブルを定義します。
(ここでは仮に users テーブルとしていますが、適宜変更してください。)

import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core";

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  fullName: text('full_name'),
  phone: varchar('phone', { length: 256 }),
});

マイグレーションの設定

drizzle.config.ts ファイルに drizzle-kit の設定を定義します。

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: './supabase/migrations',
  schema: './app/db/schema.ts',
  dialect: 'postgresql',
});

drizzle で作成したマイグレーションファイルを supabase ディレクトリに出力するのがポイントです。
supabase migration up コマンドでマイグレーションを Supabase に適用できるようになることで、ローカルの変更内容を本番環境にも簡単にマイグレーションしやすくなります。

次に、package.json にマイグレーションファイルを生成する db:generate と、マイグレーションを Supabase に適用する db:migrate script を追加します。

"scripts": {
  "db:generate": "drizzle-kit generate",
  "db:migrate": "supabase migration up"
}

マイグレーションの実行

npm run db:generate
npm run db:migrate

マイグレーションを実行し、http://localhost:54323/project/default/editor に users テーブルが追加されていれば OK です。

シードデータの投入

app/db/seeds.ts ファイルを作成します。

import { seed } from 'drizzle-seed';
import * as schema from './schema';
import dotenv from 'dotenv';
import { drizzle } from 'drizzle-orm/node-postgres';

dotenv.config();

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);
  await seed(db, schema);
}

main();

次に、package.json に db:init script を追加します。
seeds.ts では process.env.DATABASE_URL としているため .env が必要になります。
ですが、seed 作成のためだけに .env を管理するのは避けたいので、環境変数を .dev.vars から読み込めるように dotenv を利用します。

"scripts": {
  "db:init": "dotenv -e .dev.vars -- tsx ./app/db/seeds"
}

npm run db:init 実行後、http://localhost:54323/project/default/editor の users テーブルを確認して、10個の初期データが投入されていれば OK です。

初期データを取得して console に出力してみる

Supabase に登録されたデータを Cloudflare Workers 経由で取得し、フロントエンドで console.log してみます。

Drizzle のクライアントを作成

Supabase のデータベースと接続するためのクライアントを作成します。
app/db/client.server.ts を作成し、Drizzle を利用して PostgreSQL に接続できるようにします。

import postgres from 'postgres'
import { drizzle } from 'drizzle-orm/postgres-js'

export const db = (env: { DATABASE_URL: string }) => {
  const client = postgres(env.DATABASE_URL, { prepare: false })
  return drizzle(client)
}

API ルートを作成

次に、Cloudflare Workers で Supabase の users テーブルからデータを取得する API エンドポイントを作成します。
app/routes/api.users.get.tsx を作成し、データを取得して返します。

import type { Route } from "./+types/api.users.get";
import { db } from '../db/client.server';
import { users } from '../db/schema';

export async function loader({ context }: Route.LoaderArgs) {
  const dbClient = db(context.cloudflare.env);
  const usersTable = await dbClient.select().from(users);

  return { users: usersTable };
}

これで http://localhost:5173/api/users/get にアクセスすると、Supabase の users テーブルから取得したデータが返るようになります。

API ルートを React Router に追加

API ルートを認識させるために、app/routes.tsapi/users/get を追加します。

import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
  route("api/users/get", "routes/api.users.get.tsx"),
] satisfies RouteConfig;

フロントエンドでデータを取得し console に出力

最後に、フロントエンド (home.tsx) で users データを取得し、console.log で確認します。

import type { Route } from "./+types/home";
import { Welcome } from "../welcome/welcome";

export function meta({}: Route.MetaArgs) {
  return [
    { title: "New React Router App" },
    { name: "description", content: "Welcome to React Router!" },
  ];
}

export async function loader({ context }: Route.LoaderArgs) {
  const message = context.cloudflare.env.VALUE_FROM_CLOUDFLARE ;
  const res = await fetch("http://localhost:5173/api/users/get");
  const users = await res.json();

  return { message, users };
}

export default function Home({ loaderData }: Route.ComponentProps) {
  const users = loaderData.users;
  console.log(users);
  return <Welcome message={loaderData.message} />;
}

http://localhost:5173/ で検証ツールからコンソールを確認し、users が表示されていれば OK です。

最後に

Supabase CLI にはスキーマを設定するファイルがないため、Drizzle を使って TypeScript で定義できるのはとても便利でした。
型安全にデータベース操作を行え、簡単にスキーマ管理ができるのは Drizzle ORM の大きな魅力です。
Supabase との組み合わせによって、スムーズにデータベースの構築・管理が可能になります。

Gaji-Labo では、React Router をはじめとしたフロントエンド技術を活用し、スタートアップのプロダクト開発を支援していますので、興味のある方はお気軽にカジュアル面談をお申し込みください!

Gaji-Labo フロントエンドエンジニア向けご案内資料

Gaji-Labo は新規事業やサービス開発に取り組む、事業会社・スタートアップへの支援を行っています。

弊社では、Next.js を用いた Web アプリケーションのフロントエンド開発をリードするフロントエンドエンジニアを募集しています!さまざまなプロダクトやチームに関わりながら、一緒に成長を体験しませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください!

求人応募してみる!


投稿者 Sena Murakami

アシスタントフロントエンドエンジニア。 Webマーケティング会社でマークアップエンジニアとして経験を積み、Gaji-Laboに入社。デザインの意図を汲み取ったマークアップが得意です。チームを俯瞰してリードできるエンジニアになることが目標です。趣味はバドミントンやバレーボール、キャンプなど。