🌩️

Remix + Google Auth + Cloudflare Pages + Drizzle + D1 のテンプレートを作った

2024/04/30に公開

Remix で SSR するアプリが Cloudflare Pages 上で動きます。Google 認証付です。

本番で動かすならデータベースは別にしたいのですが、エッジサーバー上で SSR できる。安くて早くて美味い。そんな組み合わせです。

シンプルな構成にするために、ORMはDrizzle ORMを使っています。Prismaもエッジ環境での実行にベータ版対応しましたが、サイズがデカくなり過ぎるので見送っています。

デモサイト

https://remix-cloudflare-pages-d1-34w.pages.dev/

リポジトリ

https://github.com/ryokryok/remix-cloudflare-pages-d1-drizzle

新規作成する場合

pnpx create-remix@latest --template ryokryok/remix-cloudflare-pages-d1-drizzle

工夫した点

認証

既に先行で作成された例を参考しました。

https://github.com/mizchi/remix-d1-bullets/tree/main

現在は型安全に書きやすくなっています。

https://github.com/ryokryok/remix-cloudflare-pages-d1-drizzle/blob/main/app/services/auth.server.ts

Config から生成した型を使うようにする

Cloudflare のサービスをバインディングを型安全にするために、wrangler typesworker-configuration.d.tsを生成できます。
この型はload-context.tsで読み込まれます。

load-context.ts
import { type PlatformProxy } from "wrangler";

type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;

declare module "@remix-run/cloudflare" {
  interface AppLoadContext {
    cloudflare: Cloudflare;
  }
}

これによって入力時に型補完が効くようになります。

同一ページから複数 action がある場合

Remixでは表示するComponent, 表示時にデータ取得や読み込みをするLoader, POST時に実行するActionが定義できます。

同一ページ内で複数Actionを定義する場合はどうするの?というIssueがありました。

https://github.com/remix-run/remix/discussions/3138

個人的にデータの追加と削除は別ルーティングで行いたいのと、機能が少ないので1処理1ファイルにしても大丈夫だろうと判断しました。

Remixの独自タグのFormはactionを実行する先を変更できるのでこれでPOST先を指定します。

app/routes/_index.tsx
<Form action="/posts/create" method={"POST"}>
  <input
    type="text"
    name="post-body"
    id="post-body"
  />
  <button
    type={"submit"}
  >
    Create Post
  </button>
</Form>

対応するパスにはactionの処理を書きます。表示する必要がないのでComponentもLoaderも書きません。

最後に実行元へのredirectを返すようにします。

app/routes/posts.create.tsx
export const action = async ({ context, request }: ActionFunctionArgs) => {
  const db = getDBClient(context.cloudflare.env.DB)
  const formData = await request.formData()
  const postBody = formData.get("post-body")?.toString()
  // validation
  if (postBody === undefined || postBody.length === 0) {
    return new Response("Post body is empty", { status: 500 })
  }
    await db.insert(posts).values({ body: postBody?.toString()})
  }
  return redirect("/")
};

https://remix.run/docs/en/main/components/form#action

残っている課題

  • テスト
    • @cloudflare/vitest-pool-workers/config のテスト設定と Remix の設定を共存させたい
    • これがあると env.DB みたいな方法で D1 にアクセスしてテストが書ける
  • GitHub Workflow のデプロイ
    • 現在は wrangler.toml の設定を反映させるためにローカルから build&deploy
    • Wrangler GitHub Action でいい感じにやりたい

Remixの所感

React版Ruby on Railsみたいな「設定より規約」みたいな思想を感じるからか、フレームワークとしてのマジカル度は低めでとっつきやすくて好きです。

Discussion