はじめに
こんにちは!
今回は Next.js (React 19) と Supabase だけでサクッと動く ToDo アプリ を作ったので、
セットアップ〜実装ポイントを 1 ページにまとめました。
話題のSupabaseを触ってみたい
という方の参考になれば幸いです 📝
目次
完成イメージ
ローカルで npm run dev
→ ブラウザにアクセスするとこんな感じです。
タスク追加・削除がリアルタイムに反映されます✨
技術スタック
区分 | 採用技術 | メモ |
---|---|---|
フロント | 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 プロジェクトを作成
- Supabase Console で新規プロジェクトを作成
- 「Project Password」にメモを残す
- 「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 が更新されます。