Next.js + Supabaseでtodoアプリを作る手順

# はじめに
DBサービスのSupbaseが便利だったのでこちらにフォーカスして開発の手順を紹介します。特にテーブルを作成するだけでAPIを自動生成してくれるのが手間を減らせて良かったです。

https://supabase.com/

他でも大丈夫ですが今回はNext.jsを使います。

※ローカル環境のみです。デプロイの手順については後日追加する可能性があります

# 前提条件
ローカルに以下インストール済み
- nodejs・npm
- docker

※今回はローカル環境のみなのでsupabaseの会員登録なしでも問題ないはずです

# Next.jsプロジェクト作成 
コマンドを実行すると質問されるので今回はtypescriptとtailwindcss使用 + srcディレクトリなしにしている
```shell
npx create-next-app todo-list
```
プロジェクトディレクトリに移動
```shell
cd todo-list
```
開発サーバー起動
```shell
npm run dev
```

# SupbaseのDockerコンテナ作成

supabase CLIインストール

```shell
npm install supabase --save-dev
```

supabase初期設定コマンド実行。Denoは今回なし。コマンド実行後、プロジェクトルートにsupbaseディレクトリが作成される
```
npx supabase init
Generate VS Code settings for Deno? [y/N] n
Generate IntelliJ Settings for Deno? [y/N] n
Finished supabase init
```

ローカル開発用のsupbaseコンテナ作成・起動する
※自分の場合dockerコンテナのビルドにかなり時間かかった
完了するとターミナル上に後ほど使用する各URLとキーが表示される

```shell
❯ npx supabase start
WARN: no seed files matched pattern: supabase/seed.sql
Started supabase local development setup.

         API URL: http://127.0.0.1:54321
     GraphQL URL: http://127.0.0.1:54321/graphql/v1
  S3 Storage URL: http://127.0.0.1:54321/storage/v1/s3
          DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
      Studio URL: http://127.0.0.1:54323
    Inbucket URL: http://127.0.0.1:54324
      JWT secret: [実際の値]
        anon key: [実際の値]
service_role key: [実際の値]
   S3 Access Key: [実際の値]
   S3 Secret Key: [実際の値]
       S3 Region: local
```

Docker Desktopを見てもsupbaseのコンテナが起動していることがわかる

![スクリーンショット 2024-12-15 124125.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3520261/95d1ce00-cae6-4f47-a6f0-aad9d0db51cc.png)

# Supbaseブラウザ画面の操作

## テーブル作成

先ほど表示されたStudio URLでローカルのブラウザ画面にアクセス
```sql
Studio URL: http://127.0.0.1:54323
```
ブラウザからテーブル作成
サイドバーのTable Editor > New Tableボタンをクリック

![Default-Project-Default-Organization-Supabase-12-15-2024_12_27_PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3520261/3a736cd8-7f43-7317-314b-5ab06051ee83.png)

テーブル定義を記述してSaveする。今回は簡易なのでuser_idはなし

![Default-Project-Default-Organization-Supabase-12-15-2024_12_30_PM (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3520261/fc516ccb-6f25-5c18-ac78-bc81ef280349.png)

## データ作成

Insertボタン > Insert rowボタンからtodosテーブルにデータ投入

![スクリーンショット 2024-12-15 122826.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3520261/6d4542df-3e99-2696-9df2-0ea5334ae6ed.png)
テスト用に以下2件作成

![スクリーンショット 2024-12-15 122858.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3520261/61786f4f-25a9-794d-b53b-3b02b3607980.png)

## RLSの設定

このままだとセキュリティからデータをアプリ上で表示できないので、todosテーブルにRLS(Row Level Security)を設定する。画面右上のAdd RLS Policyボタンをクリック

![Default-Project-Default-Organization-Supabase-12-15-2024_12_30_PM (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3520261/a66f8034-087a-7030-26d0-3f49c5febbf8.png)

今回は全てユーザーの全ての操作を許可。実際のアプリでは適切な設定にする

![Auth-Supabase-12-15-2024_12_30_PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3520261/2543e662-597c-b4ad-4ff8-96418983e65d.png)

# プロジェクトでsupabaseクライアントの作成
プロジェクトのルートに.envファイルを作成
先述のnpx supabase startでターミナル上に表示されたAPI URLとanon keyを記述する
```
NEXT_PUBLIC_SUPABASE_URL=[API URL]
NEXT_PUBLIC_SUPABASE_ANON_KEY=[anon key]
```
supbasejsインストール
```
npm install @supabase/supabase-js
```

supabaseクライアント作成。ファイルはどこでも大丈夫だが、今回はsupbaseディレクトリ配下にclient.tsを作成する。先程.envに記述した環境変数を呼び出している
```ts
import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
```

supbabaseはtypescriptにも対応している
型作成、supabaseディレクトリに型ファイルを作成する
```
npx supabase gen types typescript --local > supabase/database.types.ts
```

supbaseクライアントに型追加。createClientメソッドに対してDababaseの型情報を追加している
```ts
import { createClient } from "@supabase/supabase-js";
import { Database } from "./database.types";

export const supabase = createClient<Database>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
```

# todoアプリのコード記述

app/page.tsxを以下のように編集して基本的なtodoの操作ができるようにする。
- todoの一覧表示
- todoの完了・未完了の切り替え
- todoの追加
- todoの削除

今回はNextjsのサーバーアクションを使用している。サーバーアクションを使わずクライアントコンポーネントで実装することも可能

```tsx
import { supabase } from '@/supabase/client';
import { revalidatePath } from 'next/cache';

export default async function Home() {
  const { data: todos, error } = await supabase
    .from('todos')
    .select('*')
    .order('created_at', { ascending: false });

  if (error) {
    console.error('Error fetching todos:', error);
    return <div>Error loading todos</div>;
  }

  async function addTodo(formData: FormData) {
    'use server';

    const name = formData.get('name') as string;
    if (!name.trim()) return;

    await supabase.from('todos').insert([{ name, completed: false }]);

    revalidatePath('/');
  }

  async function toggleTodo(formData: FormData) {
    'use server';

    const id = formData.get('id') as string;
    const completed = formData.get('completed') === 'true';

    await supabase.from('todos').update({ completed: !completed }).eq('id', id);

    revalidatePath('/');
  }

  async function deleteTodo(formData: FormData) {
    'use server'

    const id = formData.get('id') as string

    await supabase
      .from('todos')
      .delete()
      .eq('id', id)

    revalidatePath('/')
  }

  return (
    <div className="max-w-4xl mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">Todo List</h1>

      <form action={addTodo} className="mb-6">
        <div className="flex gap-2">
          <input
            type="text"
            name="name"
            placeholder="新しいTodoを入力..."
            className="flex-1 px-4 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
            required
          />
          <button
            type="submit"
            className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
          >
            追加
          </button>
        </div>
      </form>

      <ul className="space-y-2">
        {todos.map((todo) => (
          <li key={todo.id} className="p-4 bg-white rounded shadow flex items-center justify-between gap-2">
            <span className={todo.completed ? 'line-through' : ''}>
              {todo.name}
            </span>
            <div className="flex gap-2">
              <form action={toggleTodo}>
                <input type="hidden" name="id" value={todo.id} />
                <input type="hidden" name="completed" value={todo.completed.toString()} />
                <button
                  type="submit"
                  className={`px-3 py-1 rounded ${
                    todo.completed
                      ? 'bg-gray-500 hover:bg-gray-600'
                      : 'bg-green-500 hover:bg-green-600'
                  } text-white`}
                >
                  {todo.completed ? '未完了に戻す' : '完了'}
                </button>
              </form>
              <form action={deleteTodo}>
                <input type="hidden" name="id" value={todo.id} />
                <button
                  type="submit"
                  className="px-3 py-1 bg-red-500 hover:bg-red-600 text-white rounded"
                >
                  削除
                </button>
              </form>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}
```
http://localhost:3000/ にアクセスして実際にアプリを動かしたら以下のようになる

![画面録画-2024-12-15-125850.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3520261/9cce4a4c-f8ec-6438-9a2f-6a1e759e3872.gif)

# githubリポジトリ
本記事の手順通りに行ったコードは以下です

https://github.com/sky2432/supabase-todo

# おわりに
supbaseには無料プランがあるので、気になった方は是非使用してみてください

https://supabase.com/pricing

# 参考資料
https://supabase.com/docs/reference/javascript/typescript-support

https://supabase.com/docs/guides/database/secure-data