🤡

Cloudflare Workers × Drizzle × Turso で開発環境と本番環境に分けてDBを管理する

に公開

はじめに

最近、ReactRouter を使って個人開発を進めているのですが、 Cloudflare Workers と Drizzle と Turso を使った DB 環境構築に少し詰まったので備忘録として残しておこうと思います。

使用する技術スタックは以下のとおりです。

まだ、制作をはじめたばかりですが下記リポジトリになります。
https://github.com/otaki0413/gym-memo

やりたいこと

  • ローカル環境と本番環境で、データベースを分けて管理できるようにする(どちらもリモート上に作成)
  • Cloudflare 上のアプリから Drizzle を使って Turso 上のデータにアクセスできるようにする

0. セットアップ

私の場合、Cloudflare が提供する公式テンプレートを使いました。
また、Remix 公式 が提供するこちらのテンプレートでも問題ないと思います。

どちらのテンプレートもnpm run deployで ReactRouter アプリが Cloudflare Workers に簡単にデプロイできるようになっています。

npm create cloudflare@latest -- --template=cloudflare/templates/react-router-starter-template

1. データベース作成と接続情報の取得

今回、データベースに Turso を使用するので、DB を作成します。

開発環境と本番環境で DB を分けたいので、2 個作ります。
(※DB 作成〜認証トークン取得までを 2 回行う)

Turso にログインした状態で DB を作成します。

turso db create <データベース名>

DB が作成できたら公式の手順に従って、2 つの資格情報を取得しましょう。

データベース URL の取得

turso db show --url <データベース名>

認証トークンの取得

turso db tokens create <データベース名>

取得できたら、ルートディレクトリ直下に.dev.varsを作成して記載します。
.dev.varsは、ローカル開発時に優先的に読み込まれるファイルです。
公式に詳しく書いてあるので参考にしてみてください。

.dev.vars
TURSO_URL=<開発用のデータベースURL>
TURSO_AUTH_TOKEN=<開発用の認証トークン>

一方、本番環境で使用する環境変数は、.prod.varsというファイルを作成して記載しました。
ファイル名は何でも良いですが、.dev.varsと対照的にしたい意図でこうしています。

.prod.vars
TURSO_URL=<本番用のデータベースURL>
TURSO_AUTH_TOKEN=<本番用の認証トークン>

本来、本番環境では Cloudflare のダッシュボードで環境変数を設定しますが、
DB スキーマ変更時にマイグレーションを本番 DB に適用したいときには、ローカルのマイグレーションファイルを適用する必要があると思い、このような形をとっています。
(※もし他に良い方法があればぜひ教えていただきたいです 👏)

どちらのファイルも .gitignoreに追記しておきましょう。

.gitignore
.dev.vars*
.prod.vars*

ここでnpm run dev を実行してみましょう。
先ほど説明した通り、開発時には.dev.varsが自動的に読み込まれることがわかるはずです。

npm run dev

> dev
> react-router dev

Using vars defined in .dev.vars 👈️👈️ これ!!!
  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  Debug:   http://localhost:5173/__debug
  ➜  press h + enter to show help

Workers における環境変数の取り扱いについては、
こちらの記事公式ドキュメントに詳しく記載されているので参考にしてみてください。

https://zenn.dev/matty5791/articles/d15dbf5a80921b
https://developers.cloudflare.com/workers/local-development/environment-variables/

2. Workers 環境で扱う型定義の生成

公式ドキュメントによれば、自身の Worker に合った型定義を生成することを推奨しています。

We recommend you generate types for your Worker by running wrangler types

npm スクリプトを見てみると、wrangler typesが設定されているかと思います。
こちら実行することで、⁠worker-configuration.d.tsという型定義ファイルが生成されます。

package.json
 "scripts": {
   "typegen": "wrangler types && react-router typegen",
},

※初回時は下記

worker-configuration.d.ts(旧)
declare namespace Cloudflare {
  interface Env {
    VALUE_FROM_CLOUDFLARE: "Hello from Cloudflare";
  }
}
interface Env extends Cloudflare.Env {}

そしてtypegenコマンド実行後は、.dev.vars に設定した Turso の接続情報2つが Cloudflare という名前空間のEnvに設定されていることがわかるはずです。

worker-configuration.d.ts(新)
declare namespace Cloudflare {
	interface Env {
		VALUE_FROM_CLOUDFLARE: "Hello from Cloudflare";
		TURSO_URL: string; 👈️ これ!
		TURSO_AUTH_TOKEN: string; 👈️ これ!
	}
}
interface Env extends Cloudflare.Env {}
(このあとも続くが省略)

ここで作成した型を、以降の Drizzle の設定で使用します。

https://developers.cloudflare.com/workers/languages/typescript/#2-generate-runtime-types-using-wrangler

3. Drizzle のセットアップ

次に、アプリケーションと DB を繋ぐ ORM の設定を進めていきます。
今回は Drizzle を使用するので、公式に従って必要なパッケージをインストールします。

npm i drizzle-orm @libsql/client
npm i -D drizzle-kit

次にスキーマの作成を行います。
app/db/schema.tsを作成して、下記のようなイメージで作成しました。

app/db/schema.ts
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { cuid, createdAt, updatedAt } from "./helpers";

export const userTable = sqliteTable("user", {
  id: cuid(),
  username: text("username", { mode: "text" }).notNull().unique(),
  displayName: text("display_name", { mode: "text" }),
  email: text("email", { mode: "text" }).notNull().unique(),
  avatarUrl: text("avatar_url", { mode: "text" }),
  bio: text("bio", { mode: "text" }),
  createdAt,
  updatedAt,
});

... (省略) ...

このあと、このスキーマファイルを元にマイグレーション設定を行います。

4. マイグレーション設定

マイグレーションの設定内容はdrizzle.config.tsに記載するのですが、
今回マイグレーション適用時に.dev.vars.prod.varsのどちらの環境変数を読み込むかを切り替えたいので、下記のような実装をしました。
process.env.ENVに渡る値は、package.jsonの npm スクリプトで調整します。(以降参照)

drizzle.config.ts
import type { Config } from "drizzle-kit";
import dotenv from "dotenv";

// ENV に応じて読み込む環境変数ファイルを切替え
const currentEnv = process.env.ENV ?? "development";
console.log(`Current environment: ${currentEnv}`);
dotenv.config({
  path: currentEnv === "production" ? ".prod.vars" : ".dev.vars",
});

if (!process.env.TURSO_URL || !process.env.TURSO_AUTH_TOKEN) {
  console.error("環境変数が設定されていません。");
  process.exit(1);
}

export default {
  schema: "./app/db/schema.ts",
  out: "./drizzle",
  dialect: "turso",
  dbCredentials: {
    url: process.env.TURSO_URL,
    authToken: process.env.TURSO_AUTH_TOKEN,
  },
} as Config;

package.json に記載する npm スクリプトは下記になります。
マイグレーションの適用コマンドは、npm run db:migrate ですが
ENVを用いて、開発用 DB と本番用 DB のどちらに適用するか切り替えられるようにしました。

package.json
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:migrate:dev": "ENV=development npm run db:migrate", 👈️ 開発用DBに適用
"db:migrate:prod": "ENV=production npm run db:migrate", 👈️ 本番用DBに適用
"db:studio": "drizzle-kit studio",

5. Drizzle クライアント作成

次に Turso に接続するためのクライアントを作成します。
私の場合、app フォルダの中にdb/client.server.tsファイルを配置し、記述しました。
こちらの方の記事が参考になりました!

https://www.gaji.jp/blog/2025/02/13/22355/

app/db/client.server.ts
import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";

export const db = (env: Env) => {
  const client = createClient({
    url: env.TURSO_URL,
    authToken: env.TURSO_AUTH_TOKEN,
  });
  return drizzle(client);
};

ここで重要なのが、引数のenvの型Envです。
ここで使っている Env 型は、先ほど typegen コマンドで生成された worker-configuration.d.ts 内でグローバルに定義されているため、import せずにどこでも使用できます。

そして DB クライアントに対してdrizzle関数でラップしているといった感じになります。

https://orm.drizzle.team/docs/connect-turso

6. データ取得のイメージ

実際の ReactRouter のコードでのデータ取得イメージです。

loader関数の引数に渡るcontextから、環境変数にアクセスできるので
context.cloudflare.envを db 関数に渡して、DB クライアントを作成してデータ取得を行っています。

menus.tsx
import type { Route } from "./+types/menus";
import { db } from "~/db/client.server";
import { trainingMenuTable } from "~/db/schema";

export async function loader({ context }: Route.LoaderArgs) {
   // DBクライアント作成
  const dbClient = db(context.cloudflare.env);
  // データ取得
  const myMenus = await dbClient.select().from(trainingMenuTable);
  return { myMenus };
}

export default function Menus({ loaderData }: Route.ComponentProps) {
  const { myMenus } = loaderData;

  return (
    <div className="space-y-3 p-3">
      <div className="flex items-center justify-between">
        <div className="text-2xl font-semibold">マイメニューの管理</div>
        <div>
          <Button variant="outline" className="aspect-square max-sm:p-0">
            <PlusIcon size={16} aria-hidden="true" />
            新規作成
          </Button>
        </div>
      </div>

      {/* マイメニューリスト */}
      <MenuList initialMenus={myMenus} />
    </div>
  );
}

おわりに

本記事では、個人開発をする中で CloudflareWorkers × Drizzle × Turso の DB 環境構築で詰まった部分についてまとめてみました。
DB を開発用と本番用に分けるやり方は、本当にこれで適切なのかわかりませんがもっと良い方法があればぜひ教えていただきたいです。
ここまで読んでいただきありがとうございました。

参考サイト

https://orm.drizzle.team/docs/connect-turso
https://docs.turso.tech/sdk/ts/orm/drizzle#drizzle-turso
https://www.gaji.jp/blog/2025/02/13/22355/
https://zenn.dev/matty5791/articles/d15dbf5a80921b

GitHubで編集を提案

Discussion