Shuhei
Next.js App Router × Prisma × Supabase:複数テーブル同時作成とサインアップをAPIに寄せる
2026年05月22日
要約を生成中...
複数テーブルへの同時作成は $transaction で
フロントから1回のAPI呼び出しで複数テーブルにレコードを作るには、Route Handler + Prismaトランザクションが基本パターン。
// app/api/register/route.ts
await prisma.$transaction(async (tx) => {
const recordA = await tx.tableA.create({ data: { ... } });
// 存在チェックしてから作成
const existingB = await tx.tableB.findFirst({
where: { someUniqueField: body.someValue }
});
if (!existingB) {
await tx.tableB.create({ data: { tableAId: recordA.id, ... } });
}
});
tx はトランザクション専用のPrismaクライアント。tx 経由でSELECTもUPDATEも自由に書けるので、POSTメソッドの中でGETのAPIを別途呼ぶ必要はない。
tx とは何か
$transaction に渡すコールバック関数の引数で、トランザクション専用のPrismaクライアント。通常の prisma との違いはこう。
// 通常:それぞれ独立したDBクエリ(バラバラに実行される)
await prisma.shift.create({ ... }); // クエリ1
await prisma.assignment.create({ ... }); // クエリ2 ← shift作成が失敗しても実行される
// tx:同じトランザクション内で実行される
await prisma.$transaction(async (tx) => {
await tx.shift.create({ ... }); // クエリ1
await tx.assignment.create({ ... }); // クエリ2 ← shift失敗なら絶対実行されない
});
名前は慣習的に tx が使われるが、transaction や db でも問題ない。
upsert で「なければ作る」を1行に
await tx.tableB.upsert({
where: { teamId_name: { teamId: body.teamId, name: body.name } },
create: { ... },
update: {} // あっても何もしない
});
ただし where に指定できるのは @unique か @@unique のフィールドのみ。複合ユニークにしたい場合はスキーマに追加する。
model Role {
id Int @id @default(autoincrement())
teamId Int
name String
@@unique([teamId, name]) // これを追加
}
追加後はマイグレーションを忘れずに。
npx prisma migrate dev --name add_unique_role_team_name
Supabaseのサインアップもサーバー側に寄せられる
Admin Clientを使えばサインアップもRoute Handler内で完結する。
// lib/supabase/admin.ts
export const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
注意点:Supabaseへの登録はPrismaの tx の外なのでロールバックされない。DB失敗時にSupabaseのユーザーも削除するクリーンアップ処理を入れると安全。
const { data, error } = await supabaseAdmin.auth.admin.createUser({ ... });
if (error) throw new Error(error.message);
try {
await tx.user.create({ ... });
} catch (e) {
// DBが失敗したらSupabaseのユーザーも消す
await supabaseAdmin.auth.admin.deleteUser(data.user.id);
throw e;
}
@supabase/ssr vs @supabase/supabase-js
App Routerなら @supabase/ssr 一択。セッションのCookie管理を自動でやってくれる。
lib/supabase/
client.ts ← createBrowserClient(クライアントコンポーネント用)
server.ts ← createServerClient(Server Component / Route Handler用)
admin.ts ← supabase-js直接(SERVICE_ROLE_KEY使う場合のみ)
分かれている理由はセッションの保存場所が違うから。ブラウザはlocalStorage、サーバーはCookieを使う。
@supabase/ssr は @supabase/supabase-js のラッパーなので、内部では同じものが動いている。App Router環境でCookieベースのセッション管理をやりやすくしたのが @supabase/ssr。

