1. はじめに
この記事では Next.js と Supabase を使って、シンプルなタスク管理アプリをゼロから構築する方法を紹介します。
「とりあえず動くものをサクッと作りたい」という方向けの入門記事となります。
この記事で扱うこと
- Next.js(App Router)の基本セットアップ
- Supabase プロジェクト作成・テーブル準備
- タスクの追加・更新・削除・取得(CRUD)
- UI の実装と動作確認
この記事で扱わないこと
- Next.js の内部挙動の詳細な解説
- Supabase の高度な機能(Edge Functions, Realtime の深掘り)
- デザインの細かいカスタマイズ
- 大規模アプリの構成やアーキテクチャ
- Vercel へのデプロイ
- ログイン機能
完成イメージ
上記のようなタスク管理アプリを、一つひとつ手順を追いながら作っていきます。
2. Next のセットアップ
まずは npx を使って Next アプリを作成しましょう。
npx create-next-app@latest task-manager
ここでnpxコマンドが使えない人は、Node.js をインストールする必要があります。
Node.js のインストールはこちらの記事を参照ください。
【Windows】Nodejsをインストールしよう
Macにnodejsをインストールする方法
何か聞かれたら基本的に yes を選べば大丈夫です。
create-next-app@16.0.3
Ok to proceed? (y) -> y
以下のような選択肢が出たら、Yes, use recommended defaults を選択してください。
? Would you like to use the recommended Next.js defaults? › - Use arrow-keys. Return to submit.
> Yes, use recommended defaults
TypeScript, ESLint, Tailwind CSS, App Router
No, reuse previous settings
No, customize settings
上手くいくと、以下のようなディレクトリが作成されます
./task-manager
├── app
├── eslint.config.mjs
├── next-env.d.ts
├── next.config.ts
├── node_modules
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── README.md
└── tsconfig.json
アプリディレクトリに移動し、開発モードで Next を起動しましょう。
cd task-manager
npm run dev
ここで
http://localhost:3000
にアクセスすると、以下のような画面が出てくると思います。
これで Next のセットアップは完了です。
お疲れ様でした!
3. Supabase のセットアップ
続いて Supabase 側のセットアップをしていきます。
アカウント・Organazation の作成
まずは Supabase のアカウントを作成します。
https://supabase.com/
にアクセスし、「Start your project」を押してください。
Supabase のアカウントを持っていない場合、ここでアカウントを作成することになります。
Github アカウントでログインすることも可能です。
Organization がない場合はこちらから作成します。
Organization はプロジェクトをまとめるグループのようなものです。
とりあえず 1 つ作っておけば OK です。
プロジェクトの作成
続いてプロジェクトを作成します。
テーブルの作成
次に、タスクを保存するテーブルを作っていきましょう。
テーブル情報を入力します。
今回はこのような設計にしました。
| 列名 | 型 | デフォルト | 説明 |
|---|---|---|---|
| id | int8 | 主キーとなる数値。自動ナンバリング | |
| created_at | timestamp | now() | 作成日 |
| name | varchar | no_name | タスクの名前 |
| progress | int2 | 0 | タスクの進捗状況(0 ~ 100) |
| due_date | date | CURRENT_DATE | タスクの完了予定日 |
データを追加してみる
insert ボタンからデータを追加してみましょう。
id や created_at 、progress はデフォルト値でいいので、name と due_date だけを設定します。
以下のようにデータが追加されていれば成功です!
RLS policyの追加
最後に RLS policy を追加します。
RLS とは Row Level Security の略で、データベースに対し、どのユーザーがどの程度の権限を持って操作を行えるかを決めるものです。
RLS について詳しく知りたい方はこちらなどが参考になります。
Add RLS policy ボタンから追加していきます。
Create policy ボタンでポリシーを追加します。
今回は以下のように設定しました。
この設定は、URL と KEY からアクセスしたユーザー全員が全権限を持つ設定となります。実際の運用の際は必要に応じて細かくポリシーを設定する必要があるでしょう。
ポリシーの切り分けにも先ほどと同じくこちらのサイトが参考になります。
以上で Supabase のセットアップは終了です。
お疲れ様でした!
4. Next.js と Supabaseの連携
それでは、いよいよ Next.js と Supabase の連携を作っていきます。
必要パッケージのインストール
以下のコマンドを task-manager ディレクトリで実行し、必要なパッケージをインストールします。
npm install @supabase/supabase-js
npm install @supabase/ssr
.env ファイルの作成
先ほど作った task-manager ディレクトリに、.env ファイルを作成します。
NEXT_PUBLIC_SUPABASE_URL=あなたのURL
NEXT_PUBLIC_SUPABASE_ANON_KEY=あなたのANON_KEY
URL と KEY は、Supabase の画面から確認できます。
黒く塗りつぶされている部分があなたの URL と KEY です。
Supabase クライアントをcreateする関数を作成
Next から Supabase を操作するためには、Supabase クライアントを create する必要があります。
task-manager ディレクトリにutils フォルダを作成し、以下のような構成にしてください。
./utils
└── supabase
├── client.ts
└── server.ts
今作った server.ts と client.ts をそれぞれ書いていきましょう。
まずは server.ts です。
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
export const createClient = async () => {
const cookieStore = await cookies();
return createServerClient(
supabaseUrl!,
supabaseKey!,
{
cookies: {
async getAll() {
return await cookieStore.getAll()
},
async setAll(cookiesToSet) {
try {
for(const { name, value, options } of cookiesToSet) {
await cookieStore.set(name, value, options)
}
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
},
);
};
次に client.ts です。
import { createBrowserClient } from "@supabase/ssr";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
export const createClient = () =>
createBrowserClient(
supabaseUrl!,
supabaseKey!,
);
ここで client.ts と server.ts に 2 つの createClient 関数を作っていますが、これは Nexst のサーバーコンポーネントとクライアントコンポーネントで Supabase クライアントの扱いが異なるためです。
サーバーコンポーネントとクライアントコンポーネントについてはここでは深く言及しませんが、学びたい方はこちらなどに簡潔にまとめてあります。
pageの設定
画面用のコードを書いていきます。
task-manager/app/page.tsx を以下のように書き換えましょう。
import { createClient } from '@/utils/supabase/server'
export default async function Home() {
const supabase = await createClient();
const { data: tasks, error } = await supabase.from('task').select('*')
if (error) {
console.error(error)
return <div>データ取得に失敗しました</div>
}
return (
<div className='bg-white flex items-center justify-center h-screen'>
{tasks?.map((task) => {
return <div key={task.id} className='text-black'>{task.name}</div>
})}
</div>
);
}
http://localhost:3000
にアクセスして、先ほど Supabase で登録したデータの name が表示されていれば成功です!
ここまでできれば、Supabase 上でデータを追加・削除すると、サイト上でも追加・削除されている様子が確認できると思います。
以上でNext.js と Supabase の連携は完了です!
5. React で画面UIを作成する
本記事では React の詳細な説明は行いません。この章のプログラムの意味を知りたい方はこちら(執筆予定)の記事にまとめる予定です。
task-list ページ作成
まずは現在のタスクリストを表示する画面を作ります。
完成イメージの右側の部分です。
/task-manager/app の中に task-list というフォルダを作り、task-list/page.tsx を以下のように設定しましょう。
import { createClient } from '@/utils/supabase/server'
export default async function TaskListPage(){
// Supabase のデータ取得
const supabase = await createClient();
const { data: tasks, error } = await supabase.from('task').select('*')
if (error) {
console.error(error)
return <div>データ取得に失敗しました</div>
}
// テーブルを作成する
return (<>
{/* ページタイトル */}
<div className='w-full'>
<div className='border-b border-b-black mb-4'>
<h2 className='font-bold text-black text-3xl p-2'>Task List</h2>
</div>
</div>
{/* テーブル部分 */}
<table className='border-collapse border border-gray-300 w-full bg-red'>
<tbody>
{/* 列名表示 */}
<tr>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left' >Name</th>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left'>Created At</th>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left'>Due Date</th>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left'>Progress</th>
</tr>
{/* 各行表示 */}
{tasks?.map((task) => {
return <tr key={task.id} className='text-black'>
<td className='p-2 border border-gray-300'>{task.name}</td>
<td className='p-2 border border-gray-300'>{new Date(task.created_at).toISOString().split("T")[0]}</td>
<td className='p-2 border border-gray-300'>{task.due_date}</td>
<td className='p-2 border border-gray-300'>{task.progress}%</td>
</tr>
})}
</tbody>
</table>
</>);
}
この時点で http://localhost:3000/task-list に接続して確認すると、表示が崩れているかもしれません。
Layout の設定まで進めば解消されますので、Layoutの設定が終わったら再度確認してみてください。
Tab 作成
タブ部分を作成していきます。
task-manager/app/components/tab.tsx を作成し、以下のように設定します。
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
const tabs = [
{ name: "TaskList", href: "/task-list" },
];
export default function Tabs() {
const pathname = usePathname();
return (<>
{ tabs.map((tab) => {
const isActive = pathname == tab.href;
return (
<Link
key = {tab.name}
href = {tab.href}
className={`p-2 flex items-center border-b border-gray-300 cursor-pointer hover:bg-gray-200 ${isActive && "font-bold"}`}>
{tab.name}
</Link>
);
})}
</>);
}
新たにタブを作りたい場合は、ここの tabs に追加していきます。
Layout の設定
次に Layout の設定をします。
Layout とは、複数ページで共通のページ構成などを定義する場合に使います。
今回の場合、task-list ページと create ページはどちらも「左側にタブ、右側に main コンテンツ」という構成になっているので、その部分を Layout を用いて作成していきます。
task-manager/app/layout.tsx を以下のように編集しましょう。
import Tabs from "./components/tab";
import './globals.css';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className='flex h-screen font-mono'>
{/* 左側 */}
<aside className='w-1/8 shadow-lg bg-gray-100 text-black border-r border-gray-300'>
{/* ロゴ */}
<div className='font-serif p-6 border-b border-gray-300 flex items-center justify-center text-xl'>
Task Manager
</div>
{/* タブ */}
<Tabs />
</aside>
{/* 右側 */}
<main className='bg-gray-100 h-screen w-7/8 p-4'>
{/* main コンテンツ */}
{children}
</main>
</body>
</html>
);
}
ここで
http://localhost:3000/task-list
に接続すると、UIが作成されていることが確認できます!
ここからは、タスクの追加や削除など各種機能を作っていきましょう。
6. Create 機能の実装
この章ではタスクを追加するための create ページを作成します。
task-manager/app に create フォルダを作り、page.tsx を以下のように設定しましょう。
"use client";
import { createClient } from '@/utils/supabase/client';
import { useActionState, useState } from 'react';
export default function CreatePage(){
const supabase = createClient();
const [progress, setProgress] = useState(0);
const handleSubmit = async (prevState:unknown, formData: FormData) => {
const name = formData.get("name") as string;
const dueDate = formData.get("due_date") as string;
const progress = Number(formData.get("progress"));
const { error } = await supabase
.from("task")
.insert({
name,
due_date: dueDate,
progress,
});
if (error) {
console.log(error);
return "エラーが発生しました";
}
return "追加完了!";
}
const [ feedback, formAction, isPending ] = useActionState(handleSubmit, "");
// テーブルを作成する
return (<>
{/* ページタイトル */}
<div className='w-full'>
<div className='border-b border-b-black mb-4'>
<h2 className='font-bold text-black text-3xl p-2'>Create</h2>
</div>
</div>
{/* 追加フォーム */}
<form action={formAction} className='text-black'>
{/* Name */}
<div className="grid grid-cols-2 items-center gap-2 w-1/3 my-2">
<label htmlFor="name" className='text-black'>Name</label>
<input type="text" name="name" className='border border-gray-300 p-2'/>
</div>
{/* Due_data */}
<div className="grid grid-cols-2 items-center gap-2 w-1/3 my-2">
<label htmlFor="due_date" className='text-black'>Due Date</label>
<input type="date" name="due_date" className='border border-gray-300 p-2'/>
</div>
{/* Progress */}
<div className="grid grid-cols-2 items-center gap-2 w-1/3 my-2">
<label htmlFor="progress" className='text-black'>Progress({progress})</label>
<input type="range" name="progress" min={0} max={100} defaultValue={progress} className='border border-gray-300' onChange={(e) => setProgress(Number(e.target.value))}/>
</div>
{/* 送信ボタン */}
<button className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 block my-4">保存</button>
{isPending && <p>送信中...</p>}
{!isPending && <p>{feedback}</p>}
</form>
</>);
}
長めなコードですが、やっていることはシンプルです。
名前、完了予定日など各項目について input を作成し、送信のタイミングで supabase に insert する設計となっています。
途中で出てきた useActionState フックについては公式サイトがわかりやすいです。
Create タブを追加するため、先ほどの /task-manmager/app/components/tab.tsx を少し編集しましょう。
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
const tabs = [
{ name: "TaskList", href: "/task-list" },
+ { name: "Create", href: "/create"},
];
export default function Tabs() {
const pathname = usePathname();
return (<>
{ tabs.map((tab) => {
const isActive = pathname == tab.href;
return (
<Link
key = {tab.name}
href = {tab.href}
className={`p-2 flex items-center border-b border-gray-300 cursor-pointer hover:bg-gray-200 ${isActive && "font-bold"}`}>
{tab.name}
</Link>
);
})}
</>);
}
これでタブ部分に Create が追加されたはずです!
実際にタスクが追加できることも確認してみましょう。
タスクの内容を設定して「保存」ボタンを押すと...
追加されました!
7. Delete 機能の実装
この章では、タスク削除のボタンを作成していきます。
/task-manager/app/task-list/page.tsx を以下のように変更してみましょう。
import { createClient } from '@/utils/supabase/server'
+import { revalidatePath } from 'next/cache';
export default async function TaskListPage(){
// Supabase のデータ取得
const supabase = await createClient();
const { data: tasks, error } = await supabase.from('task').select('*')
if (error) {
console.error(error)
return <div>データ取得に失敗しました</div>
}
+ // 指定されたIDのタスクを削除
+ const handleDelete = async (formData: FormData) => {
+ "use server"
+ const id = formData.get("id") as string;
+ const supabase = await createClient();
+ await supabase.from('task').delete().eq('id', id)
+
+ // 削除後にページを再取得
+ revalidatePath("/task-list")
+ }
// テーブルを作成する
return (<>
{/* ページタイトル */}
<div className='w-full'>
<div className='border-b border-b-black mb-4'>
<h2 className='font-bold text-black text-3xl p-2'>Task List</h2>
</div>
</div>
{/* テーブル部分 */}
<table className='border-collapse border border-gray-300 w-full bg-red'>
<tbody>
{/* 列名表示 */}
<tr>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left' >Name</th>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left'>Created At</th>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left'>Due Date</th>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left'>Progress</th>
+ <th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left'>Delete</th>
</tr>
{/* 各行表示 */}
{tasks?.map((task) => {
return <tr key={task.id} className='text-black'>
<td className='p-2 border border-gray-300'>{task.name}</td>
<td className='p-2 border border-gray-300'>{new Date(task.created_at).toISOString().split("T")[0]}</td>
<td className='p-2 border border-gray-300'>{task.due_date}</td>
<td className='p-2 border border-gray-300'>{task.progress}%</td>
+ <td className='p-2 border border-gray-300'>
+ <form action={handleDelete}>
+ <input type="hidden" name="id" value={task.id} />
+ <button className="text-red-500 cursor-pointer">
+ DELETE
+ </button>
+ </form>
+ </td>
</tr>
})}
</tbody>
</table>
</>);
}
handleDelete で出てきた use server は、この関数が「サーバー側で実行される関数ですよ」という宣言です。
なぜ必要なのかはここでは言及しませんが、こちらなどで解説されています。
これで削除機能は完成です!
試してみましょう。
上手くいってますね!
8. Update 機能の実装
最後に、タスク変更の機能を作っていきます。
まずは /task-manager/app/details/page.tsx を作成し、以下のようにします。
"use client";
import { createClient } from "@/utils/supabase/client";
import { useState, useActionState } from "react";
import { useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
export default function DetailsPage() {
// URLパラメータから、各値をとってくる
const query = useSearchParams();
const [progress, setProgress] = useState<number>(Number(query.get('progress')));
const defaultName = query.get('name') ?? "";
const defaultDue_date = query.get('due_date') ?? "2000-01-01";
const id = query.get('id');
// 指定されたIDのタスクを更新
const handleUpdate = async (prevState:unknown, formData: FormData) => {
const name = formData.get("name") as string;
const due_date = formData.get("due_date") as string;
const supabase = createClient();
await supabase.from('task').update({name, due_date, progress}).eq("id", id);
// 変更が終わったらタスクリストに戻る
const router = useRouter();
router.push('/task-list')
return "変更完了!";
}
const [ feedback, formAction, isPending ] = useActionState(handleUpdate, "");
return (<>
{/* Updateフォーム */}
<form action={formAction} className='text-black'>
{/* Name */}
<div className="grid grid-cols-2 items-center gap-2 w-1/3 my-2">
<label htmlFor="name" className='text-black'>Name</label>
<input type="text" name="name" className='border border-gray-300 p-2' defaultValue={defaultName}/>
</div>
{/* Due_data */}
<div className="grid grid-cols-2 items-center gap-2 w-1/3 my-2">
<label htmlFor="due_date" className='text-black'>Due Date</label>
<input type="date" name="due_date" className='border border-gray-300 p-2' defaultValue={defaultDue_date}/>
</div>
{/* Progress */}
<div className="grid grid-cols-2 items-center gap-2 w-1/3 my-2">
<label htmlFor="progress" className='text-black'>Progress({progress})</label>
<input type="range" name="progress" min={0} max={100} defaultValue={progress} className='border border-gray-300' onChange={(e) => setProgress(Number(e.target.value))}/>
</div>
{/* 送信ボタン */}
<button className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 block my-4">保存</button>
{isPending && <p>送信中...</p>}
{!isPending && <p>{feedback}</p>}
</form>
</>);
}
一見複雑そうですが、処理としてはほとんど create ページと同じです。
デフォルト値の設定は URL のクエリパラメータから送るようにしています。
クエリパラメータとは?
クエリパラメータとは、URL に付帯してやり取りされる情報のことです。例えば、
http://hogehoge.com/?name=yamada
というリンクがあったとき、クエリパラメータは「name=yamada」の部分です。
今回のコードでは、
http://localhost:3000/details?name=勉強&due_date=2025-01-01&progress=15
のように設定することで、details ページに情報を送っています。
最後に、task-list ページ → details ページへの遷移を作れば完成です!
/task-manager/app/task-list/page.tsx を少し編集しましょう。
import { createClient } from '@/utils/supabase/server'
import { revalidatePath } from 'next/cache';
import Link from 'next/link';
export default async function TaskListPage(){
// Supabase のデータ取得
const supabase = await createClient();
const { data: tasks, error } = await supabase.from('task').select('*')
if (error) {
console.error(error)
return <div>データ取得に失敗しました</div>
}
// 指定されたIDのタスクを削除
const handleDelete = async (formData: FormData) => {
"use server"
const id = formData.get("id") as string
const supabase = await createClient();
await supabase.from('task').delete().eq('id', id)
// 削除後にページを再取得
revalidatePath("/task-list")
}
// テーブルを作成する
return (<>
{/* ページタイトル */}
<div className='w-full'>
<div className='border-b border-b-black mb-4'>
<h2 className='font-bold text-black text-3xl p-2'>Task List</h2>
</div>
</div>
{/* テーブル部分 */}
<table className='border-collapse border border-gray-300 w-full bg-red'>
<tbody>
{/* 列名表示 */}
<tr>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left' >Name</th>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left'>Created At</th>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left'>Due Date</th>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left'>Progress</th>
<th className='border border-gray-300 p-2 bg-black text-white font-bold w-1/4 text-left'>Delete</th>
</tr>
{/* 各行表示 */}
{tasks?.map((task) => {
return <tr key={task.id} className='text-black'>
<td className='p-2 border border-gray-300'>
+ <Link href={{
+ pathname: '/details/',
+ query:{
+ id: task.id,
+ name: task.name,
+ due_date: task.due_date,
+ progress: task.progress,
+ }
+ }}>
+ {task.name}
+ </Link>
</td>
<td className='p-2 border border-gray-300'>{new Date(task.created_at).toISOString().split("T")[0]}</td>
<td className='p-2 border border-gray-300'>{task.due_date}</td>
<td className='p-2 border border-gray-300'>{task.progress}%</td>
<td className='p-2 border border-gray-300'>
<form action={handleDelete}>
<input type="hidden" name="id" value={task.id} />
<button className="text-red-500 cursor-pointer">
DELETE
</button>
</form>
</td>
</tr>
})}
</tbody>
</table>
</>);
}
お疲れ様でした!!!
以上でシンプルなタスク管理アプリは完成となります!
http://localhost:3000/task-list
に接続すると、ちゃんと動いていることが確認できるはずです。
9. おわりに
今回は、Next.js と Supabase を使って、シンプルなタスク管理アプリを作成しました。
入門編ということで、あえて複雑な設定や高度な実装は避けましたが、必要になったときに深掘りできるよう参考リンクを多めに載せています。
基礎的なセットアップから実際に動くアプリになるまでを一通り体験して、「あ、Next.js と Supabaseって意外と簡単に動かせるんだ」と思ってもらえたら嬉しいです。
この続きを発展させれば、認証、リアルタイム処理、デプロイなど、もっと実用的なアプリにも挑戦できます。ぜひあなたのサービス作りにも活かしてみてください!





















