はじめに
初めてNext.js(App Router)でSupabaseを使ってみたとき、まだ解説が少なく苦戦した💦
この記事ではなるべく丁寧にNext.js × Supabaseの「導入方法」と「基本的な使い方」をまとめる😊
対象読者
- App RouterでSupabaseを使ってみたい!
- Supabaseを全然知らない!
この記事で解説すること
【読み飛ばし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-appSupabaseのパッケージをインストール
- 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という名前のテーブルを作成する。
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の管理画面で結果を確認する。
応用的なデータベース操作をするには
✅他にもさまざまな関数が用意されている。
応用的な操作をしたい場合は公式ドキュメントを参照する!
まとめ
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公式のサンプルリポジトリ










































