はじめに
初めて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操作や認証を素早く開発できる。
その他にも特徴はあるが今回は省略。
使用イメージ
直感的にデータベースが使える
こんな感じの自作したテーブルがあるとする。
直感的にデータを取得できる✨
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 '@/*'
作成するとこんな感じになる。
プロジェクトに移動
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」がある。
ルートディレクトリに「.env.local」という名前の空ファイルを作成する。
そこに「URL」と「API Key」を記載する。
NEXT_PUBLIC_SUPABASE_URL=先ほど確認したURL
NEXT_PUBLIC_SUPABASE_ANON_KEY=先ほど確認したanon Key
こんな感じ✨
テーブルを作る
✅まずはTodoを保存するためのテーブルを作る。
テーブルを作成
「New table」からテーブルを新規作成。
todos
という名前のテーブルを作成する。
【補足】外部キーは「Foreign Keys」から追加できる。
※今回は外部キーは不要だが念の為補足。
todos
テーブルが完成!
テーブルの型定義ファイルを生成
✅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クライアントが生成できる。
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クライアントが生成できる。
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=テスト」のデータを挿入
完成イメージ
【手順1】入力フォームを作成
✅シンプルな「テキスト入力」と「ボタン」のフォームを作る。
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)を作る。
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() // 全データを取得する
完成イメージ
【手順1】データを表示するページを作成
todosテーブルの全データを表示するページを作る。
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」のデータ
完成イメージ
【手順1】データ更新できるフォームを作成
✅全データの「テキスト入力」と「ボタン」のフォームを作る。
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)を作る。
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」のデータ
完成イメージ
【手順1】データ削除できるフォームを作成
✅全データの「テキスト」と「ボタン」のフォームを作る。
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)を作る。
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公式のサンプルリポジトリ