1
1

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 2025-05-21

はじめに

こんにちは!
今回は Next.js (React 19)Supabase だけでサクッと動く ToDo アプリ を作ったので、
セットアップ〜実装ポイントを 1 ページにまとめました。

話題のSupabaseを触ってみたい

という方の参考になれば幸いです 📝


目次

  1. 完成イメージ
  2. 技術スタック
  3. フォルダ構成
  4. 環境変数を準備
  5. Supabase プロジェクトを作成
  6. テーブル todo を作成
  7. Next.js 実装
  8. 動かしてみる

完成イメージ

ローカルで npm run dev → ブラウザにアクセスするとこんな感じです。
タスク追加・削除がリアルタイムに反映されます✨
image.png


技術スタック

区分 採用技術 メモ
フロント Next.js 15 / React 19 App Router & Server Actions
スタイリング Tailwind CSS className だけで完結
バックエンド Supabase Postgres + Auth + Row Level Security
TypeScript

フォルダ構成

repo-root/
├─ app/             # Next.js App Router
│  └─ page.tsx      # 1 ページ構成
├─ utils/
│  └─ supabase.ts   # Supabase クライアント
├─ .env.local       # 環境変数
└─ README.md

最小構成なので 1 ファイルずつ把握すれば OK です。


環境変数を準備

.env.local に公開 URL / anon key を貼り付けます。

NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

ポイント

  • NEXT_PUBLIC_ で始まるキーはクライアントに露出するため 漏洩リスクのない anon key を使います。

Supabase プロジェクトを作成

  1. Supabase Console で新規プロジェクトを作成
  2. 「Project Password」にメモを残す
  3. 「Settings → API」で URL / anon key を取得

数十秒で PostgreSQL + 認証サーバー + ストレージ が起動します。便利すぎる…。


テーブル todo を作成

SQL エディタに貼り付けて実行するだけです。

create table public.todo (
  id    bigint generated by default as identity primary key,
  title text   not null,
  created_at timestamptz default now()
);

-- RLS を有効化(今回は簡略化のため全員読み書き可)
alter table todo enable row level security;
create policy "Public CRUD" on todo
  for all using (true) with check (true);

Next.js 実装

1. Supabase クライアント

utils/supabase.ts
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

2. ToDo 画面

app/page.tsx
"use client";

import { useState, useEffect, FormEvent } from "react";
import { supabase } from "../utils/supabase";

type Todo = { id: number; title: string };

export default function Home() {
  const [tasks, setTasks] = useState<Todo[]>([]);
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);

  // 一覧取得
  const fetchTodos = async () => {
    setLoading(true);
    const { data, error } = await supabase
      .from("todo")
      .select("*")
      .order("id", { ascending: false });
    if (!error && data) setTasks(data);
    setLoading(false);
  };

  useEffect(() => { fetchTodos(); }, []);

  // 追加
  const addTask = async (e: FormEvent) => {
    e.preventDefault();
    if (!input.trim()) return;
    setLoading(true);
    await supabase.from("todo").insert({ title: input.trim() });
    setInput("");
    await fetchTodos();
    setLoading(false);
  };

  // 削除
  const deleteTask = async (id: number) => {
    setLoading(true);
    await supabase.from("todo").delete().eq("id", id);
    await fetchTodos();
    setLoading(false);
  };

  return (
    <div className="flex flex-col items-center min-h-screen p-8 bg-gray-50 dark:bg-gray-900">
      <h1 className="text-3xl font-bold mb-8 mt-8 text-gray-800 dark:text-gray-100">
        ToDo アプリ
      </h1>

      <form onSubmit={addTask} className="flex gap-2 mb-8 w-full max-w-md">
        <input
          className="flex-1 px-4 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800 dark:text-white"
          placeholder="新しいタスクを入力..."
          value={input}
          onChange={e => setInput(e.target.value)}
        />
        <button
          type="submit"
          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
          disabled={loading}
        >
          追加
        </button>
      </form>

      <ul className="w-full max-w-md space-y-2">
        {loading && <li className="text-gray-400 text-center">読み込み中...</li>}
        {!loading && tasks.length === 0 && (
          <li className="text-gray-400 text-center">タスクがありません</li>
        )}
        {tasks.map(t => (
          <li key={t.id} className="flex justify-between bg-white dark:bg-gray-800 px-4 py-2 rounded shadow">
            <span className="break-all text-gray-800 dark:text-gray-100">{t.title}</span>
            <button
              onClick={() => deleteTask(t.id)}
              className="ml-4 text-red-500 hover:text-red-700 font-bold"
              aria-label="タスクを削除"
              disabled={loading}
            >
              削除
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

動かしてみる

# 1. プロジェクト作成
npx create-next-app@latest supabase-todo --ts --tailwind

# 2. 必要パッケージ
cd supabase-todo
npm install @supabase/supabase-js

# 3. 上記コードを貼り付け & .env.local を配置

# 4. 起動
npm run dev

http://localhost:3000 にアクセスしてタスクを追加すると、
即座に Postgres に INSERT → fetchTodos で再取得され UI が更新されます。


1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?