React × Supabase × Vercel で TODO アプリを作ってみた ① ～作ってデプロイ編～ 👷

## React × Supabase × Vercel シリーズ

ある程度、独立した内容になっていますが、ご興味あればどうぞ 💁‍♂️

- ① 作ってデプロイ編 ← 本記事
- [② Vitest で単体テスト編](https://qiita.com/kamata-bug-factory/items/e095d38e445e76210e8d)
- [③ GitHub Actions で CI/CD 編](https://qiita.com/kamata-bug-factory/items/cb7dceacba1d0b58990b)

## はじめに

旧 Twitter 新 X を見てる感じ、個人開発をしている人は **Supabase** と **Vercel** をよく使っているイメージがあります。
個人でサクッと Web アプリを作りたいなら、こいつらを採用すると良いのか？ 🤔

TODO アプリを題材にして、実際に Supabase と Vercel を触ってみようのシリーズです ✊

フロントエンドは **React** で書きました。

## Supabase, Vercel って？ 🤔

どちらも **無料枠があります！**
とてもうれしいですね ☺️

### Supabase

**Firebase の代替として注目されているオープンソースの BaaS (Backend as a Service)**

PostgreSQL ベースの RDB や認証機能などが使えます。サーバレス API も作れます。

https://supabase.com/

### Vercel

**フロントエンドの Web アプリを簡単にデプロイ・ホスティングできるプラットフォームサービス**

GitHub などのバージョン管理ツールと連携して、push するだけで自動でデプロイ(CI/CD) してくれます。
プルリクエストごとに自動でプレビュー環境を作るとかもできます。

https://vercel.com/

## 成果物 🏁

https://github.com/kamata-bug-factory/typescript-todo

### ディレクトリ構成

```bash
typescript-todo
├── README.md
└── todo-app
    ├── README.md
    ├── eslint.config.js
    ├── index.html
    ├── package-lock.json
    ├── package.json
    ├── public
    │   └── vite.svg
    ├── src
    │   ├── App.css
    │   ├── App.tsx
    │   ├── assets
    │   │   └── react.svg
    │   ├── components
    │   │   ├── CreateTodoForm
    │   │   │   ├── CreateTodoForm.css
    │   │   │   └── CreateTodoForm.tsx
    │   │   ├── TodoListItem
    │   │   │   ├── TodoListItem.css
    │   │   │   └── TodoListItem.tsx
    │   │   └── ValueViewer
    │   │       ├── ValueViewer.css
    │   │       └── ValueViewer.tsx
    │   ├── hooks
    │   │   └── useTodo.ts
    │   ├── index.css
    │   ├── lib
    │   │   └── supabaseClient.ts
    │   ├── main.tsx
    │   ├── pages
    │   │   └── TodoHome
    │   │       ├── TodoHome.css
    │   │       └── TodoHome.tsx
    │   ├── types
    │   │   └── Todo.ts
    │   ├── utils
    │   │   ├── TodoUtil.ts
    │   │   └── api.ts
    │   └── vite-env.d.ts
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.node.json
    └── vite.config.ts
```

## 作り方 🔨

### 1. プロジェクトのセットアップ

お馴染みのおまじない 🪄
ビルドツールには Vite を使っています。

```bash
npm create vite@latest todo-app --template react-ts
cd todo-app
npm install
```

:::note alert
`create-react-app` は公式で非推奨となりました。
https://github.com/facebook/create-react-app/pull/17003
:::

### 2. Supabase プロジェクトの作成とテーブル設計

[Supabase](https://supabase.com/) にログインして、新しいプロジェクトを作成します 🚀

![スクリーンショット 2025-04-11 10.02.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3848132/624cd41e-26f0-488b-8979-6af390c4e996.png)

#### テーブル設計

テーブル名: `todos`

カラム構成は以下の通り

| カラム名   | 型          | 属性                       |
| ---------- | ----------- | -------------------------- |
| id         | int8        | Primary Key, AutoIncrement |
| text       | text        | not null                   |
| done       | bool        | default false              |
| created_at | timestamptz | default now()              |
| updated_at | timestamptz | default now()              |

`updated_at` は UPDATE の度に現在時刻で更新したいので、SQL Editor で以下のクエリを実行します。

```sql
create extension if not exists moddatetime schema extensions;

create trigger handle_updated_at before update on todos
  for each row execute procedure moddatetime (updated_at);
```

デフォルトで RLS（Row Level Security）が有効なはずなので、RLS Policy を作成する必要があります。

"Add RLS Policy" をポチッ

![add-rls.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3848132/1e26debd-a3d7-4120-a7a1-64c7ae35f798.png)

"Create Policy" もポチッと押して

![create-policy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3848132/38daed2e-6199-4211-b5d9-adf9c41af25d.png)

以下のようなポリシーを追加しましょう 💁‍♂️

![スクリーンショット 2025-04-11 10.24.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3848132/8acf2f1c-ad06-472e-9a94-26fa432fd79d.png)

### 3. Supabase クライアントの設定

Supabase と接続するのに必要なパッケージをインストール

```bash
npm install @supabase/supabase-js
```

開発環境用に `.env.local` を用意し、環境変数を定義します。

```txt:.env.local
VITE_SUPABASE_URL=https://xxx.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
```

この子たちは Settings > Data API で確認できます 👀

![api-settings.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3848132/13373631-dc61-4068-b079-f89aca55fb59.png)

クライアントインスタンス `supabaseClient` を作ります。

```ts:src/lib/supabaseClient.ts
import { createClient } from "@supabase/supabase-js";

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL as string;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY as string;

export const supabaseClient = createClient(supabaseUrl, supabaseAnonKey);
```

### 4. CRUD 処理の実装

超シンプルな CRUD です。

```ts:src/utils/todoApi.ts
import { supabaseClient } from '../lib/supabaseClient';
import { Todo } from '../types/Todo';

const supabase = supabaseClient;

export class TodoApi {
  async readItems(): Promise<Todo[]> {
    const { data, error } = await supabase
      .from('todos')
      .select('id, text, done')
      .order('id', { ascending: false });

    if (error) throw error;
    return data as Todo[];
  }

  async createItem(item: Omit<Todo, 'id'>) {
    const { error } = await supabase.from('todos').insert(item);
    if (error) throw error;
  }

  async updateItem(item: Todo) {
    const { error } = await supabase
      .from('todos')
      .update(item)
      .eq('id', item.id);
    if (error) throw error;
  }

  async deleteItem(id: number) {
    const { error } = await supabase.from('todos').delete().eq('id', id);
    if (error) throw error;
  }
}
```

### 5. React アプリの実装

割愛。リポジトリ見てください 🙇‍♂️

https://github.com/kamata-bug-factory/typescript-todo

実装が終わったら GitHub に push しておきます。

:::note info
開発サーバーは `npm run dev` で起動します。
:::

### 6. Vercel にデプロイ

[Vercel](https://vercel.com/) にログインして、新しいプロジェクトを作成します 🚀

以下のように設定してデプロイ！

![vercel-new-project.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3848132/6e9fb74c-2515-4b45-9bde-816a78de6f1c.png)

:::note warn
ここで設定している環境変数は、本番環境用です。
開発環境と本番環境で DB を分けたい場合は、それぞれ別の Supabase プロジェクトを作ってください。
:::

デプロイ成功しました 🎉

![スクリーンショット 2025-04-11 11.29.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3848132/ac60df9b-318c-489f-93d1-31f0e3c9e0e5.png)

実際、作ったアプリがデプロイされていました ✌️

![スクリーンショット 2025-04-11 11.30.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3848132/a0dd7bd2-02cb-4395-b53d-b0ae266f0b46.png)

#### CI/CD

デプロイしているブランチに変更を push すると、Vercel は自動で再デプロイしてくれます。

試しに、タイトル部分を赤字にしてみます。

```html:src/pages/TodoHome/TodoHome.tsx
<h1>Todo</h1>
```

```diff_css:src/index.css
h1 {
  font-size: 2em;
+ color: red;
}
```

赤くなりました！

![スクリーンショット 2025-04-11 11.43.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3848132/d189ddef-46cd-4db3-950d-45d642be0251.png)

## おわりに

めちゃくちゃ簡単！
Vercel では最小構成の CI/CD も提供されていて、非常に便利だなと思いました。

テストを pass したときだけデプロイしたい場合には、**GitHub Actions** などで CI/CD パイプラインを構築する必要があるようです。
チャレンジしてみます ✊