0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-15

はじめに

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

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

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

前提条件

ローカルに以下インストール済み

  • nodejs・npm
  • docker

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

Next.jsプロジェクト作成

コマンドを実行すると質問されるので今回はtypescriptとtailwindcss使用 + srcディレクトリなしにしている

npx create-next-app todo-list

プロジェクトディレクトリに移動

cd todo-list

開発サーバー起動

npm run dev

SupbaseのDockerコンテナ作成

supabase CLIインストール

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とキーが表示される

❯ 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

Supbaseブラウザ画面の操作

テーブル作成

先ほど表示されたStudio URLでローカルのブラウザ画面にアクセス

Studio URL: http://127.0.0.1:54323

ブラウザからテーブル作成
サイドバーのTable Editor > New Tableボタンをクリック

Default-Project-Default-Organization-Supabase-12-15-2024_12_27_PM.png

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

Default-Project-Default-Organization-Supabase-12-15-2024_12_30_PM (1).png

データ作成

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

スクリーンショット 2024-12-15 122826.png
テスト用に以下2件作成

スクリーンショット 2024-12-15 122858.png

RLSの設定

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

Default-Project-Default-Organization-Supabase-12-15-2024_12_30_PM (2).png

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

Auth-Supabase-12-15-2024_12_30_PM.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に記述した環境変数を呼び出している

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の型情報を追加している

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のサーバーアクションを使用している。サーバーアクションを使わずクライアントコンポーネントで実装することも可能

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

githubリポジトリ

本記事の手順通りに行ったコードは以下です

おわりに

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

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?