はじめに
初めてNext.js(App Router)でSupabaseを使ってみたとき、まだ解説が少なく苦戦した💦
この記事ではなるべく丁寧にNext.js × Supabaseの「導入方法」と「基本的な使い方」をまとめる😊
対象読者
- App RouterでSupabaseを使ってみたい!
- Supabaseを全然知らない!
この記事で解説すること
-
環境構築
- Next.js(App Router)のプロジェクトを新規作成する。
- そこにSupabaseを導入する。
-
App RouterでSupabaseのデータベースを使う方法
- その中でも最も基本的なCRUD操作のやり方を解説する。
- これを見れば簡易なTodoアプリが作れるようになる✨
(完成イメージ)
【読み飛ばしOK】Supabaseって何?
最初にSupabaseをざっくり紹介する😊
すでに知っていれば読み飛ばしてOK!
- PostgreSQLを使ったBaaS。
- CRUD操作や認証を素早く開発できる。
その他にも特徴はあるが今回は省略。
使用イメージ
直感的にデータベースが使える
こんな感じの自作したテーブルがあるとする。
![Image in a image block](/notion/8eeefb93-5a2a-444b-bbe5-de7684061ba9/Untitled.png)
直感的にデータを取得できる✨
const { data, error } = await supabase
.from('note')
.select()
認証が簡単
たったこれだけでサインアップができる✨
await supabase.auth.signUp({email,password})
さらにメールでのログインだけでなく、GoogleやGitHubでのログインも簡単に実装できる!
環境構築
使用する技術
事前にインストールしておくもの
- Node.js
- npm
今からインストールするもの
- Next.js
- Supabase
Next.jsのプロジェクトを新規作成
supabase-sample-app
という名前のプロジェクトを作成する。
npx create-next-app supabase-sample-app --ts --no-tailwind --eslint --app --src-dir --import-alias '@/*'
作成するとこんな感じになる。
![Image in a image block](/notion/647b8c0f-fc3c-4d7b-9083-3ae015278c3a/Untitled.png)
プロジェクトに移動
supabase-sample-app
に移動する。
cd supabase-sample-app
Supabaseのパッケージをインストール
-
JavaScript上でSupabaseを使うためのパッケージをインストールする。
npm install @supabase/supabase-js
-
Next.jsでSupabaseを使うためのヘルパーをインストールする。
npm install @supabase/ssr
-
CLIのパッケージもインストールする。
npm install supabase --save-dev
※Supabaseのローカル開発環境を作るとき、テーブルの型定義ファイルを生成するときなどに必要。
Supabaseのプロジェクトを作成
環境変数を設定
「Home」を開いて、少し下にスクロールすると「URL」と「API Key」がある。
![Image in a image block](/notion/056bd099-ee81-4623-bad5-6395865d456e/Untitled.png)
ルートディレクトリに「.env.local」という名前の空ファイルを作成する。
そこに「URL」と「API Key」を記載する。
NEXT_PUBLIC_SUPABASE_URL=先ほど確認したURL
NEXT_PUBLIC_SUPABASE_ANON_KEY=先ほど確認したanon Key
こんな感じ✨
![Image in a image block](/notion/6079d978-8a86-441f-863f-358db1cadcc9/Untitled.png)
テーブルを作る
✅まずはTodoを保存するためのテーブルを作る。
テーブルを作成
「New table」からテーブルを新規作成。
![Image in a image block](/notion/1b0c14b0-86c6-4f89-866c-35cfad36cb7c/Untitled.png)
todos
という名前のテーブルを作成する。
![Image in a image block](/notion/cce9d079-892f-4385-bdbd-e60e213e6c4c/Untitled.png)
【補足】外部キーは「Foreign Keys」から追加できる。
※今回は外部キーは不要だが念の為補足。
![Image in a image block](/notion/16228fd4-793a-4d2b-8ac4-23a37e7c300c/Untitled.png)
todos
テーブルが完成!
![Image in a image block](/notion/b802b7f6-0001-4e5c-b2a1-bc88c667cacb/Untitled.png)
テーブルの型定義ファイルを生成
✅TypeScriptでtodos
テーブルを使うために、型定義ファイルdatabase.types.ts
が必要。
-
管理画面でReferense IDを調べる。
-
ターミナル(コマンドプロンプト)を開いてSupabaseにログイン
npx supabase login
-
型定義ファイルを生成する。
(
[Referense ID]
の部分は自分のリファレンスID
に置き換える)npx supabase gen types typescript --project-id [Reference ID] > database.types.ts
実行した場所にdatabase.types.tsが生成される✨
-
database.types.tsをsrc/types/database.types.tsに移動する。
※typesフォルダは新規作成。
App RouterでSupabaseのDBを使う
✅先ほどインストールした「@supabase/ssr」を使えば簡単にtodos
テーブルを操作できる✨
Supabaseとやり取りする変数を準備する
プログラム上でSupabaseを使うには?
✅「Supabaseクライアント」と言われる変数を介してSupabaseとやりとりする。
App Routerの注意点
✅2種類のSupabaseクライアントを使い分ける必要がある!
-
サーバー用のSupabaseクライアント
💡使用タイミング
- サーバーコンポーネント
- Server Actions
- ルートハンドラ
-
クライアント用のSupabaseクライアント
💡使用タイミング
- クライアントコンポーネント
Supabaseクライアントを生成(サーバーコンポーネント用)
✅createClient(…)
でSupabaseクライアントが生成できる。
![Image in a image block](/notion/eb0dbe6e-bb94-4601-a044-2db1a9aa61a0/Untitled.png)
src/app/utils/supabase/server.ts
※utils、supabaseフォルダは新規作成。
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";
import { Database } from "@/types/database.types";
export const createClient = () => {
const cookieStore = cookies();
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options });
} catch (error) {
// The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: "", ...options });
} catch (error) {
// The `delete` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
},
);
};
※公式ドキュメントのとおりコピペすればOK。ただしTypeScriptの特性を活かすために戻り値は明示的にDatabase型を指定するよう変更した。
Supabaseクライアントを生成(クライアントコンポーネント用)
✅先ほどと同様createClient(…)
でSupabaseクライアントが生成できる。
![Image in a image block](/notion/0f2ba123-3665-4e31-8ad0-190b7e747449/Untitled.png)
src/app/utils/supabase/server.ts
import { createBrowserClient } from "@supabase/ssr";
import { Database } from "@/types/database.types";
export const createClient = () =>
createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
※公式ドキュメントのとおりコピペすればOK。ただしTypeScriptの特性を活かすために戻り値は明示的にDatabase型を指定するよう変更した。
データ挿入
使用する関数
✅supabase.from(テーブル名).insert(データ)
でデータを挿入できる。
例
const { error } = await supabase
.from('todos') // todosテーブルに
.insert({ text: "テスト" }) // 「text=テスト」のデータを挿入
完成イメージ
![Image in a image block](/notion/9a52cb7d-b329-47a4-8ce6-071977baf087/Untitled.png)
【手順1】入力フォームを作成
✅シンプルな「テキスト入力」と「ボタン」のフォームを作る。
![Image in a image block](/notion/ef781de2-e709-4936-943c-c3e535fa8a0d/Untitled.png)
![Image in a image block](/notion/cbc15842-1799-4bd0-ac48-a27a13d2188b/Untitled.png)
src/app/insert/page.tsx
※utils、supabaseフォルダは新規作成。
'use client'
import { useState } from 'react';
import { insertData } from "./actions"; // 後で作るので今はエラーになる
const Page = () => {
// 挿入するデータ
const [text, setText] = useState('');
return (
<main>
<form action={insertData}>
<input type='text' value={text} name='text' onChange={ (e: React.ChangeEvent<HTMLInputElement>)=>setText(e.target.value) } />
<button type='submit'>追加</button>
</form>
</main>
);
}
export default Page;
※元々あるpage.tsxは削除してOK。
【手順2】データ挿入処理を作成
✅クリック時のデータ挿入処理(Server Actions)を作る。
![Image in a image block](/notion/2581d28a-1880-4048-baf9-acf208a65599/Untitled.png)
src/app/insert/actions.ts
'use server'
// サーバー側の処理なので、サーバー側のSupabaseクライアントを使用
import { createClient } from '@/utils/supabase/server'
/**
* データ挿入
* @param formData - フォームデータ
*/
export async function insertData(formData: FormData) {
// Supabaseクライアントを作成
const supabase = await createClient()
// フォームから入力値を取得
const inputs = {
text: formData.get('text') as string,
}
// データ挿入
const { error } = await supabase
.from('todos') // todosテーブルに
.insert({ text: inputs.text }) // 入力されたテキストを挿入
// エラーが発生した場合
if (error) {
// ...
}
}
動作確認
-
アプリを起動する。
npm run dev
-
http://localhost:3000/insertを開き、適当なデータを追加してみる。
-
Supabaseの管理画面で結果を確認する。
💡正常に動作していることが確認できた!
-
この後の動作確認をしやすくするために3件データを追加しておく。
データ取得
使用する関数
✅supabase.from(テーブル名).select()
でデータを取得できる。
例
const { data, error } = await supabase
.from('todos') // todosテーブルの
.select() // 全データを取得する
完成イメージ
![Image in a image block](/notion/7776725b-deb1-4f2b-b87d-58910b35233d/Untitled.png)
【手順1】データを表示するページを作成
todosテーブルの全データを表示するページを作る。
![Image in a image block](/notion/3a5f9aa4-1d19-41b5-b109-02ce37cba25b/Untitled.png)
src/app/select/page.tsx
※selectフォルダは新規作成。
// サーバー側の処理なので、サーバー側のSupabaseクライアントを使用
import { createClient } from '@/utils/supabase/server'
// このページをSSRにする(これがないと本番環境でこのページはSSGになる。その結果データベースを更新しても反映されなくなる。※supabaseとは関係なく、App Routerのお話)
export const revalidate = 0;
const Page = async () => {
// Supabaseクライアントを作成
const supabase = createClient();
// Todoのリストを取得
const { data: todos, error } = await supabase
.from('todos')
.select()
// エラーが発生した場合
if (error) {
return <div>Todoの取得でエラーが発生しました</div>
}
return (
<main>
{todos.length > 0 &&
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
}
</main>
);
}
export default Page
動作確認
-
アプリを起動する。
npm run dev
-
http://localhost:3000/selectを開くと全データが表示される。
データ更新
使用する関数
✅supabase.from(テーブル名).update(データ).eq(対象データ)
でデータを更新できる。
例
const { error } = await supabase
.from('todos') // todosテーブルの
.update({ text: 'あああ' }) // 対象データを「text=あああ」に更新する
.eq('id', 1) // 対象は「id=1」のデータ
完成イメージ
![Image in a image block](/notion/879e32a2-e443-4451-a5b1-fe4c8671b7f1/Untitled.png)
![Image in a image block](/notion/7570c9b9-2e9a-4687-a7cd-25b649728bb8/Untitled.png)
【手順1】データ更新できるフォームを作成
✅全データの「テキスト入力」と「ボタン」のフォームを作る。
![Image in a image block](/notion/4ffba014-6e4a-488d-bbda-6cd0671dda90/Untitled.png)
![Image in a image block](/notion/05874e0b-5023-4258-8df1-5623cf78b6bb/Untitled.png)
src/app/update/page.tsx
※updateフォルダは新規作成。
import { updateData } from './actions' // 後で作るので今はエラーになる
// サーバー側の処理なので、サーバー側のSupabaseクライアントを使用
import { createClient } from '@/utils/supabase/server'
// このページをSSRにする(App Routerの仕様で、これがないと本番環境でこのページはSSGになる。その結果データベースを更新しても反映されなくなる。)
export const revalidate = 0;
const Page = async () => {
// Supabaseクライアントを作成
const supabase = createClient();
// Todoのリストを取得
const { data: todos, error } = await supabase
.from('todos')
.select()
// エラーが発生した場合
if (error) {
return <div>Todoの取得でエラーが発生しました</div>
}
return (
<main>
{todos.length > 0 &&
<ul>
{/* データの数だけフォームを用意 */}
{todos.map(todo => (
<li key={todo.id}>
<form action={updateData}>
<input type='text' defaultValue={todo.text!} name='text' />
<input type='number' defaultValue={todo.id} name='id' hidden />
<button type='submit'>更新</button>
</form>
</li>
))}
</ul>
}
</main>
);
}
export default Page
※今回はuseStateによる入力値の状態管理はしていない。必要に応じてuseStateを使ってもいい。ただしその場合はクライアントコンポーネントになるので、クライアント側のSupabaseクライアントに置き換える。(使い方は同じ)
【手順2】データ更新処理を作成
✅クリック時のデータ更新処理(Server Actions)を作る。
![Image in a image block](/notion/ca3f913d-2361-4214-821a-acb5b81d8eb9/Untitled.png)
src/app/insert/actions.ts
'use server'
// サーバー側の処理なので、サーバー側のSupabaseクライアントを使用
import { createClient } from '@/utils/supabase/server'
/**
* データ更新
* @param formData - フォームデータ
*/
export async function updateData(formData: FormData) {
// Supabaseクライアントを作成
const supabase = await createClient()
// フォームから入力値を取得
const inputs = {
id: formData.get('id') as string,
text: formData.get('text') as string,
}
// データ更新
const { error } = await supabase
.from('todos') // todosテーブルを
.update({ text: inputs.text }) // 入力されたテキストに更新する
.eq('id', parseInt(inputs.id)) // 対象はidが一致するデータ
// エラーが発生した場合
if (error) {
// ...
}
}
動作確認
-
アプリを起動する。
npm run dev
-
http://localhost:3000/updateを開き、適当なデータを更新してみる。
-
Supabaseの管理画面で結果を確認する。
データ削除
使用する関数
✅supabase.from(テーブル名).delete().eq(対象データ)
でデータを削除できる。
例
const { error } = await supabase
.from('todos') // todosテーブルから
.delete() // 対象データを削除する
.eq('id', 1) // 対象は「id=1」のデータ
完成イメージ
![Image in a image block](/notion/31a03ee1-5b6f-4c65-9c79-1b3720a6fca9/Untitled.png)
![Image in a image block](/notion/dc2cc894-809b-4640-990d-09af3fba8917/Untitled.png)
![Image in a image block](/notion/9f049d23-a342-4454-af59-3524beaa8f3a/Untitled.png)
【手順1】データ削除できるフォームを作成
✅全データの「テキスト」と「ボタン」のフォームを作る。
![Image in a image block](/notion/dbd7eb72-a116-4bcb-b0e2-e6382c84b948/Untitled.png)
![Image in a image block](/notion/721098d3-9a6b-4c6a-abe7-e2c3baa091e1/Untitled.png)
src/app/delete/page.tsx
※deleteフォルダは新規作成。
import { deleteData } from './actions' // 後で作るので今はエラーになる
// サーバー側の処理なので、サーバー側のSupabaseクライアントを使用
import { createClient } from '@/utils/supabase/server'
// このページをSSRにする(これがないと本番環境でこのページはSSGになる。その結果データベースを更新しても反映されなくなる。)
export const revalidate = 0;
const Page = async () => {
// Supabaseクライアントを作成
const supabase = createClient();
// Todoのリストを取得
const { data: todos, error } = await supabase
.from('todos')
.select()
// エラーが発生した場合
if (error) {
return <div>Todoの取得でエラーが発生しました</div>
}
return (
<main>
{todos.length > 0 &&
<ul>
{/* データの数だけフォームを用意 */}
{todos.map(todo => (
<li key={todo.id}>
<form action={deleteData}>
<input type='text' defaultValue={todo.text!} name='text' disabled />
<input type='number' defaultValue={todo.id} name='id' hidden />
<button type='submit'>削除</button>
</form>
</li>
))}
</ul>
}
</main>
);
}
export default Page
【手順2】データ削除処理を作成
✅クリック時のデータ削除処理(Server Actions)を作る。
![Image in a image block](/notion/a5c0d158-eb22-4dfa-810f-892a135a4cad/Untitled.png)
src/app/delete/actions.ts
'use server'
import { revalidatePath } from "next/cache";
// サーバー側の処理なので、サーバー側のSupabaseクライアントを使用
import { createClient } from '@/utils/supabase/server'
/**
* データ削除
* @param formData - フォームデータ
*/
export async function deleteData(formData: FormData) {
// Supabaseクライアントを作成
const supabase = await createClient()
// フォームから入力値を取得
const inputs = {
id: formData.get('id') as string,
text: formData.get('text') as string,
}
// データ削除
const { error } = await supabase
.from('todos') // todosテーブルから
.delete() // 対象データを削除する
.eq('id', parseInt(inputs.id)) // 対象はidが一致するデータ
// エラーが発生した場合
if (error) {
// ...
}
// ページを再検証する(最新のデータを取得しなおす)
revalidatePath("/delete");
}
動作確認
-
アプリを起動する。
npm run dev
-
http://localhost:3000/deleteを開き、適当なデータをしてみる。
-
Supabaseの管理画面で結果を確認する。
応用的なデータベース操作をするには
✅他にもさまざまな関数が用意されている。
応用的な操作をしたい場合は公式ドキュメントを参照する!
- Postgresの関数呼び出し:https://supabase.com/docs/reference/javascript/rpc
- フィルター:https://supabase.com/docs/reference/javascript/using-filters
- 修飾子:https://supabase.com/docs/reference/javascript/using-modifiers
まとめ
App RouterでSupabaseを使ってみた。
今回は「Server Actions」を使って実装したが、他にも「ルートハンドラ」や「クリックイベント」でSupabaseを使うパターンもありそう💭
CRUD操作まとめ
// 挿入
supabase.from(テーブル名).insert(データ)
// 取得
supabase.from(テーブル名).select()
// 更新
supabase.from(テーブル名).update(データ).eq(対象データ)
// 削除
supabase.from(テーブル名).delete().eq(対象データ)
参考サイト
公式ドキュメント(関数)
公式ドキュメント(Next.jsの環境構築)
環境構築が分かりやすい
Next.js公式のサンプルリポジトリ