メインコンテンツへスキップ
ブログ一覧に戻る
バックエンド開発

PrismaとSupabaseを使ったバックエンド構築の基礎

2023年12月10日
10分で読めます

この記事でわかること

モダンなバックエンド開発においてPrisma ORMとSupabaseを活用したデータベース設計と実装方法について解説します。

PrismaとSupabaseを組み合わせるときは、先に「誰が、どの接続経路で、何を変更できるか」を決めます。Prismaはアプリケーションからの問い合わせを扱うORMで、SupabaseはPostgreSQLや認証などの機能を提供します。両者を導入しただけで、利用者ごとの権限制御まで完成するわけではありません。

30秒で要点

  • 教材はPrisma ORM 7を対象にします。旧版の設定例と混ぜず、生成先・接続設定・アダプターをそろえます。
  • 通常の問い合わせとスキーマ変更では、接続先と必要権限が異なります。接続文字列はサーバー側だけで扱います。
  • User/Postの作成・関連付け・公開記事の取得を順に確認します。外部DBへの接続やマイグレーションの成功は、この教材の検証範囲に含みません。
画面、認可するサーバー、通常DB接続、変更用DB接続の責任を分けたPrisma教材図
著者作成。通常の問い合わせとスキーマ変更は接続と権限を別に確認し、DBロールと利用者の認可を混同しません。

接続より先に責任範囲を分ける

架空の社内記事システムを考えます。画面から届いた「投稿者ID」をそのまま信用せず、認証済みセッションから利用者を特定し、その人に投稿権限があるかをサーバーで判断します。その後でPrismaに保存を依頼します。投稿一覧を公開するなら、公開状態の絞り込みと、返してよいフィールドの選択も必要です。

表が収まらない場合は、横にスクロールできます

層主な役割ここだけでは確認できないこと
画面入力と結果の表示リクエスト送信者の権限
サーバー認証・認可、入力検証、処理の判断DB接続ロールの過剰権限
Prisma型のある問い合わせと関連データ操作業務上のアクセス可否
PostgreSQL / Supabaseデータ保存、制約、実行ロールに応じた権限制御画面の入力意図
SupabaseのPrismaガイドには、RLSを迂回できるロールを作る例があります。この経路での問い合わせが、利用者のJWTに対応したRLSポリシーで自動的に絞られると考えてはいけません。RLSの公式説明も、テーブル権限と行のポリシー、特権ロールを区別しています。Data APIを使う経路とサーバーのDB接続経路を一覧にし、それぞれの拒否ケースを確認します。

1. 版をそろえて教材用の構成を用意する

以下はPrisma 7向けの構成例です。別の作業用プロジェクトで使用し、Node.jsなどの動作要件も対象版の公式資料で確認してください。実際には解決されたパッチ版をロックファイルに固定します。この記事の編集では、パッケージの導入やSupabaseプロジェクトの作成を行っていません。

npm install --save-dev prisma@7
npm install @prisma/client@7 @prisma/adapter-pg@7 pg dotenv
npx prisma init

Prisma 7ではESMを前提とする構成を確認し、package.jsonのtypeをmoduleにします。版を指定したPrisma Clientの説明に合わせ、接続URLは設定ファイル、モデルはスキーマ、問い合わせは生成されたClientという配置にします。

2. UserとPostの関係を表す

prisma/schema.prismaの例です。メールの一意制約、投稿者との関係、公開状態の初期値を明示します。nameとcontentの疑問符は値がない場合を許す指定です。

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())
}

この構造は「投稿者が存在すること」をDB制約で扱いますが、「今リクエストしている人がその投稿者として操作してよいこと」は表しません。制約と認可を別々に設計する理由です。

3. 実行用と変更用の接続を選ぶ

次の値は接続できない教材用プレースホルダーです。実値を公開リポジトリやブラウザー用の環境変数へ入れないでください。パスワードの特殊文字は接続URLとして適切にエンコードします。

DATABASE_URL="postgresql://APP_ROLE:REPLACE_ME@RUNTIME_HOST:5432/postgres"
MIGRATION_DATABASE_URL="postgresql://MIGRATION_ROLE:REPLACE_ME@MIGRATION_HOST:5432/postgres"

長時間稼働するサーバーか短命な接続が多い構成か、IPv6が利用できるか、利用するドライバーが対応するかで接続方式を選びます。Supabaseの案内では、セッションプールとトランザクションプールは別の用途です。ポート番号だけを書き換えて同等とみなさず、管理画面が示すホスト・ユーザー名・方式を確認します。変更用接続にはマイグレーションやshadow databaseに必要な権限・環境を別途用意します。

prisma.config.tsは変更用のURLを読みます。通常のアプリケーション用URLと同じものを使うかどうかも、権限と接続方式を確認して決めます。

import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: { path: "prisma/migrations" },
  datasource: { url: env("MIGRATION_DATABASE_URL") },
});

4. 変更をレビューしてから適用する

以下は自分が管理する開発用DBでの手順例であり、本番環境向けの一括実行手順ではありません。生成SQLに削除・名前変更・既存データへの制約追加がないかを確認し、必要なら適用前に修正します。

npx prisma migrate dev --name init --create-only
# 生成された migration.sql と対象DBを確認してから実行する
npx prisma migrate dev
npx prisma generate

Prisma 7のマイグレーション資料では、migrate devを開発用途に限定しています。変更作成にはshadow databaseへの処理もあるため、--create-onlyを「接続不要・副作用なし」と解釈しません。本番適用は、レビュー済みの変更をmigrate deployなどの承認済み手順で扱い、復元計画と実施記録を用意します。失敗時に自動でresetする手順はここでは採りません。

5. サーバーのClientと作成処理をつなぐ

src/db.tsの例です。リクエストごとに無制限にClientを増やさず、プロセス内で共有する方針を決めます。開発時の再読み込みや実行基盤に合う管理方法は別途必要です。

import "dotenv/config";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "./generated/prisma/client";

const connectionString = process.env.DATABASE_URL;
if (!connectionString) throw new Error("DATABASE_URL is required");
export const prisma = new PrismaClient({
  adapter: new PrismaPg({ connectionString }),
});

利用者の作成例です。空白だけの名前や空のメールを拒否しますが、この短い例はメール到達性や完全なアドレス仕様の検査ではありません。重複メールによるDBの一意制約エラーも、呼び出し側で適切な表示へ変換します。

import { prisma } from "./db";

export async function createUser(name: string, email: string) {
  const cleanName = name.trim();
  const cleanEmail = email.trim();
  if (!cleanName || !cleanEmail) throw new Error("name and email are required");
  return prisma.user.create({
    data: { name: cleanName, email: cleanEmail },
  });
}

続いて投稿作成と公開一覧です。actorIdはフォームの自由入力ではなく、認証・認可を終えたサーバー処理から渡す約束にします。下書きで保存し、公開操作は別の承認処理に分けています。

import { prisma } from "./db";

export async function createPost(actorId: number, title: string, content: string) {
  if (!Number.isInteger(actorId) || actorId <= 0 || !title.trim()) {
    throw new Error("invalid post input");
  }
  return prisma.post.create({
    data: { title: title.trim(), content, authorId: actorId, published: false },
    include: { author: { select: { id: true, name: true } } },
  });
}

export async function getPosts() {
  return prisma.post.findMany({
    where: { published: true },
    select: {
      id: true, title: true, createdAt: true,
      author: { select: { id: true, name: true } },
    },
    orderBy: [{ createdAt: "desc" }, { id: "desc" }],
  });
}

同時刻の投稿にはIDを第2の並び順として使います。例の公開結果にメールや本文全量は含めていません。管理画面用の取得が必要なら、公開一覧を流用せず、その用途の認可と返却項目を定義します。

小さな確認票で次の実装へ進む

表が収まらない場合は、横にスクロールできます

確認教材で用意する入力期待する判断
入力境界空白の名前、空タイトル、負の投稿者IDDB呼び出し前に拒否
公開範囲公開2件・下書き1件の仮想データ一覧に公開2件だけを返す
並び順同時刻の公開記事2件IDの降順で安定させる
権限未ログイン、他人の投稿編集実サーバーと実DBで別途拒否確認
変更作業データのある開発DBSQLレビューと復元手順を確認

この記事では入力拒否や問い合わせ条件をローカルの代替DBオブジェクトで検証します。その結果は、Supabaseへの実接続、生成Clientの実DB動作、RLSや本番権限の受入とは別です。導入前にデータベースの基礎とAPIの役割も整理すると、責任の分担を決めやすくなります。