本文へスキップ

Next.jsでPrismaからSupabaseを操作してみる

はじめに

Prismaを使用してSupabaseを操作する簡易的なToDoリストをNext.jsで構築してみます。

主に使用するものは以下になります。

  • Next.js v14.2.5(App Router)
  • Prisma v5.18.0
  • Prisma Client v5.18.0
  • Supabase
  • TypeScript v5
  • shadcn/ui(Tailwind CSS v3.4.1)
注意

この記事の手順とコードは、書いた2024年8月時点の上記のバージョンのものです。2026年10月時点の最新版では、次の点が変わっています。最新版で試す場合は、それぞれの公式ドキュメントも確認してください。

  • Prisma 7から、Prisma Clientの作成にドライバーアダプター(PostgreSQLは@prisma/adapter-pg)が必須になりました。データベースの接続先はschema.prismaではなくprisma.config.tsに書き、generatorのproviderはprisma-clientが推奨です。
  • Next.js 15から、Route HandlerのparamsはPromiseになりました。const { id } = await params;のようにawaitしてから使います。
  • shadcn/uiのCLIは、パッケージ名がshadcn-uiからshadcnに変わりました。新しいプロジェクトはTailwind CSS v4が前提で、初期化のときの質問も変わっています。

Upgrade to Prisma ORM 7 | Prisma Documentation⁠(新しいタブで開く)

Prisma | Supabase Docs⁠(新しいタブで開く)

環境構築〜マイグレーション

  1. 以下のコマンドを実行しプロジェクトを作成します。ページのデザインを整えるためにshadcn/uiを追加でインストールしています。
    ターミナルbash · 3行
    npx create-next-app@latest my-app --typescript --tailwind --eslintcd my-appnpx shadcn@latest init
  2. コマンド実行後の質問には以下のように回答します。
    テキストテキスト · 9行
    # Next.js✔ Would you like to use `src/` directory? › Yes✔ Would you like to use App Router? (recommended) › Yes✔ Would you like to customize the default import alias (@/*)? › No# shadcn/ui✔ Which style would you like to use? › Default✔ Which color would you like to use as base color? › Slate✔ Would you like to use CSS variables for colors? … yes
  3. このあとの実装で使うshadcn/uiのコンポーネント(Button・Input・Checkbox・Card)を追加します。
    ターミナルbash · 1行
    npx shadcn@latest add button input checkbox card
  4. Supabaseのダッシュボードにアクセスし、[New Project]を押し任意の組織(Organization)を選択します。

    Supabase⁠(新しいタブで開く)

    Supabaseの新規プロジェクト作成ボタン
    プロジェクト作成時、該当する組織名を選択する
  5. プロジェクト名とパスワードを設定し、[Create new project]を押下します。
    Supabaseの新規プロジェクト設定画面
    プロジェクト名、パスワード、地域を設定する
  6. 次に以下のコマンドを実行してPrismaをインストールして初期化します。
    ターミナルbash · 2行
    npm install prisma --save-devnpx prisma init
  7. Prismaからデータベースを操作するので、Prisma Clientも合わせてインストールします。
    ターミナルbash · 1行
    npm install @prisma/client
  8. Supabaseで[Project Settings] > [Database]の[Connection String]を開き[URI]を選択し、記載されているURIをコピーして、接続モード別に.envのDATABASE_URLとDIRECT_URLに設定してください。URIの[YOUR-PASSWORD]は、プロジェクトの作成時に設定したパスワードに置き換えます。

    Connection StringでURIを選択する。Modeを切り替えて環境変数にそれぞれのURIを設定する。

    Connection StringでURIを選択する。Modeを切り替えて環境変数にそれぞれのURIを設定する。
    .env.env · 2行
    DATABASE_URL="" # Transaction接続プーラーのURIを設定します。DIRECT_URL="" # Session接続プーラーのURIを設定します。
    補足

    2026年10月時点のSupabaseでは、接続文字列はプロジェクトのダッシュボードの[Connect]から確認します。Transaction接続プーラーのURIはポート番号が6543、Session接続プーラーのURIは5432です。DATABASE_URLに設定するTransaction接続プーラーのURIには、末尾に?pgbouncer=trueを付けます。

  9. prisma/schema.prismaを開き、datasourceにdirectUrlを足して、必要なモデルを定義します。
    schema.prismaPrisma · 21行
    generator client {  provider = "prisma-client-js"}datasource db {  provider  = "postgresql"  url       = env("DATABASE_URL")  directUrl = env("DIRECT_URL")}// 以下の記述を追加してください。model Task {  id        String  @id @default(cuid())  title     String  completed Boolean @default(false)  createdAt DateTime @default(now()) @map("created_at")  updatedAt DateTime @updatedAt @map("updated_at")  @@map("task")}
  10. 以下のコマンドを実行し、データベースマイグレーションを作成します。
    ターミナルbash · 1行
    npx prisma migrate dev --name init
    • --name initでマイグレーションに「init」という名前をつけています。
  11. 実行が完了すると、ほぼリアルタイムでSupabaseのTable Editorに追加したテーブルが反映されていることが確認できます。

    SupabaseのTable Editorにschema.prismaで設定したモデルが反映されている

    SupabaseのTable Editorにschema.prismaで設定したモデルが反映されている

APIの作成

シングルトンパターンを導入し、アプリケーション全体で単一のPrismaClientインスタンスを使用するようにします。

シングルトンパターン|フロントエンドのデザインパターン⁠(新しいタブで開く)

  1. 以下のコマンドを実行して、Prisma Clientの生成をおこないます。
    ターミナルbash · 1行
    npx prisma generate
  2. src/libにprisma.tsを作成し、以下のコードを追加しシングルトンパターンを実現します。
    prisma.tsTypeScript · 15行
    import { PrismaClient } from '@prisma/client'const prismaClientSingleton = () => {  return new PrismaClient()}declare const globalThis: {  prismaGlobal: ReturnType<typeof prismaClientSingleton>;} & typeof global;const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()export default prismaif (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma

    コードは、Prismaの公式ドキュメントの記述に従っています。

    Comprehensive Guide to Using Prisma ORM with Next.js (Prisma ORM v7)⁠(新しいタブで開く)

  3. src/app/api/tasksにroute.tsを作成し、以下のコードを追加します。
    route.tsTypeScript · 27行
    import prisma from "@/lib/prisma";import { type NextRequest, NextResponse } from "next/server";export async function GET(req: NextRequest) {  try {    const tasks = await prisma.task.findMany();    return NextResponse.json(tasks);  } catch (error) {    console.error("Request error", error);    return NextResponse.json({ error: "Error fetching tasks" }, { status: 500 });  }}export async function POST(req: NextRequest) {  try {    const body = await req.json();    const newTask = await prisma.task.create({      data: {        title: body.title,      },    });    return NextResponse.json(newTask, { status: 201 });  } catch (error) {    console.error("Request error", error);    return NextResponse.json({ error: "Error creating tasks" }, { status: 500 });  }}
  4. src/app/api/tasks/[id]にroute.tsを作成し、以下のコードを追加します。
    route.tsTypeScript · 34行
    import { type NextRequest, NextResponse } from "next/server";import prisma from "@/lib/prisma";export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {  try {    const id = params.id;    const { completed } = await req.json();    const updatedTask = await prisma.task.update({      where: { id },      data: { completed },    });    return NextResponse.json(updatedTask);  } catch (error) {    console.error("Error updating task:", error);    return NextResponse.json({ error: "Failed to update task" }, { status: 500 });  }}export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) {  try {    const id = params.id;    await prisma.task.delete({      where: { id },    });    return NextResponse.json({ message: "Task deleted successfully" });  } catch (error) {    console.error("Error deleting task:", error);    return NextResponse.json({ error: "Failed to delete task" }, { status: 500 });  }}

ページの作成と実装

  1. src/componentsにAddTask.tsxを作成し、以下のコードを追加します。
    AddTask.tsxTSX · 39行
    "use client";import { Task } from "@prisma/client";import React, { useState } from "react";import { Button } from "@/components/ui/button";import { Input } from "@/components/ui/input";interface Props {  onAdd: (title: Task["title"]) => void;}export default function AddTask({ onAdd }: Props) {  const [title, setTitle] = useState<Task["title"]>("");  /**   * 新しいタスクを追加する   *   * @param e - フォーム送信イベント   */  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {    e.preventDefault();    if (title.trim()) {      onAdd(title);      setTitle("");    }  };  return (    <form onSubmit={handleSubmit} className="flex space-x-2">      <Input        type="text"        value={title}        onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTitle(e.target.value)}        placeholder="Add a new task"        className="flex-grow"      />      <Button type="submit">Add</Button>    </form>  );}
  2. src/componentsにTaskItem.tsxを作成し、以下のコードを追加します。
    TaskItem.tsxTSX · 24行
    import { Task } from "@prisma/client";import React from "react";import { Checkbox } from "@/components/ui/checkbox";import { Button } from "@/components/ui/button";interface Props {  task: Task;  onToggle: (id: Task["id"]) => void;  onDelete: (id: Task["id"]) => void;}export default function TaskItem({ task, onToggle, onDelete }: Props) {  return (    <div className="flex items-center justify-between py-2">      <div className="flex items-center space-x-2">        <Checkbox checked={task.completed} onCheckedChange={() => onToggle(task.id)} />        <span className={task.completed ? "line-through" : ""}>{task.title}</span>      </div>      <Button variant="destructive" size="sm" onClick={() => onDelete(task.id)}>        Delete      </Button>    </div>  );}
  3. src/componentsにTaskList.tsxを作成し、以下のコードを追加します。
    TaskList.tsxTSX · 106行
    "use client";import { Task } from "@prisma/client";import React, { useEffect, useState } from "react";import AddTask from "@/components/AddTask";import TaskItem from "@/components/TaskItem";import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";export default function TaskList() {  const [tasks, setTasks] = useState<Task[]>([]);  useEffect(() => {    fetchTasks();  }, []);  /**   * タスク一覧を取得する   *   * @throws {Error} タスクの取得に失敗した場合   */  const fetchTasks = async () => {    try {      const res = await fetch("/api/tasks");      const data: Task[] = await res.json();      setTasks(data);    } catch (error) {      console.error("タスクの取得に失敗しました:", error);    }  };  /**   * 新しいタスクを追加する   *   * @param title - 新しいタスクのタイトル   * @throws {Error} タスクの追加に失敗した場合   */  const addTask = async (title: Task["title"]) => {    try {      const res = await fetch("/api/tasks", {        method: "POST",        headers: { "Content-Type": "application/json" },        body: JSON.stringify({ title }),      });      const newTask: Task = await res.json();      setTasks([...tasks, newTask]);    } catch (error) {      console.error("タスクの追加に失敗しました:", error);    }  };  /**   * タスクの完了状態を切り替える   *   * @param id - 更新するタスクのID   * @throws {Error} タスクの更新に失敗した場合   */  const toggleTask = async (id: Task["id"]) => {    try {      const task = tasks.find((task) => task.id === id);      if (!task) return;      const res = await fetch(`/api/tasks/${id}`, {        method: "PUT",        headers: { "Content-Type": "application/json" },        body: JSON.stringify({ completed: !task.completed }),      });      if (res.ok) {        setTasks(tasks.map((task) => (task.id === id ? { ...task, completed: !task.completed } : task)));      }    } catch (error) {      console.error("タスクの切り替えに失敗しました:", error);    }  };  /**   * タスクを削除する   *   * @param id - 削除するタスクのID   * @throws {Error} タスクの削除に失敗した場合   */  const deleteTask = async (id: Task["id"]) => {    try {      const res = await fetch(`/api/tasks/${id}`, { method: "DELETE" });      if (res.ok) {        setTasks(tasks.filter((task) => task.id !== id));      }    } catch (error) {      console.error("タスクの削除に失敗しました:", error);    }  };  return (    <Card className="w-full max-w-md mx-auto mt-10">      <CardHeader>        <CardTitle>ToDo List</CardTitle>      </CardHeader>      <CardContent>        <AddTask onAdd={addTask} />        <div className="mt-4 space-y-2">          {tasks.map((task) => (            <TaskItem key={task.id} task={task} onToggle={toggleTask} onDelete={deleteTask} />          ))}        </div>      </CardContent>    </Card>  );}
  4. 最後にsrc/app/page.tsxにTaskListコンポーネントを追加します。
    page.tsxTSX · 9行
    import TaskList from "@/components/TaskList";export default function Home() {  return (    <div>      <TaskList />    </div>  );}

動作確認

以下のToDo Listが作成できました。

完成したToDoリストのページキャプチャ

完成したToDoリストのページキャプチャ
  • タスクを追加

    タスクを追加するとフォームの下に入力したタスクが追加されます。

    タスクを追加した状態

    タスクを追加した状態

    Supabaseにもタスクが追加されていることが確認できます。

    Supabaseにもタスクが追加されている

    Supabaseにもタスクが追加されている
  • タスクを完了

    タスク名の横のチェックボックスにチェックを入れるとタスクが完了となり、取り消し線が表示されます。

    タスクを完了した状態

    タスクを完了した状態

    Supabaseを見ると対象のタスクのcompletedの値がTRUEになっていることが確認できます。

    completedがTRUEになっている

    completedがTRUEになっている
  • タスクを削除

    タスク名の右側のDeleteボタンを押すとタスクが削除され、リストから無くなります。

    Supabaseを見ると対象のタスクが削除されていることが確認できます。

  • 複数タスクの管理

    最後にタスクを複数追加し、より実際の利用に近い形で検証をおこないます。

    複数のタスクを登録したToDoリスト

    複数のタスクを登録したToDoリスト

    Supabaseも念のため確認しておきます。

    複数のタスクが登録されている

    複数のタスクが登録されている

    一通り確認できたら、これでPrismaとSupabaseを使用したNext.jsのアプリケーション作成が完了になります。

schema.prismaのコードフォーマット

Visual Studio Codeで自動フォーマットをするために、settings.jsonに以下の記述を追加してください。

settings.jsonJSON · 5行
{  "[prisma]": {    "editor.defaultFormatter": "Prisma.prisma"  }}

合わせて以下の拡張機能も追加してください。

Prisma - Visual Studio Marketplace⁠(新しいタブで開く)

参考・引用