表示言語の切り替え

Topics

Supabase×Prismaでデータベースに接続してみる(Gemini CLI使っちゃうわよ!)

  • column

翔さんやはっしーとかがSupabaseを利用して何やら面白そうな開発ごとをしています。 今までのウォーターフォール型の開発とは違い、UIから機能要件定義をなんやらかんやら聞こえてきますが、私も負けてはいられない!

ということで、今回は簡単なメモアプリを作成し、SupabaseとPrismaを連携させてデータをデータベースに保存する手順を備忘録としてまとめてみます。Gemini CLIも使っちゃうわよ!

環境:Next.js

SupabaseのAuth、Realtime、RLSは使わず、データベース機能をAPIから利用するため Prisima を採用します。データベースのテーブルを Javascript / Typescriptのオブジェクトのように扱えるのでフロントエンドエンジニアに馴染みのある記述ができるのではないかと。

Prisma

Prisma ORMは、TypeScriptやJavaScriptで利用できるORM (Object-Relational Mapping) ライブラリです。

簡単に言うと、プログラミング言語のオブジェクトで定義したメソッドで、SQLを書かずにデータベースの操作が可能なツールです。

さらにPrismaはDBのスキーマから型を自動生成してくれるので、データベースに存在しないカラムや型違いのデータを操作しようとすると、コンパイルエラーで気づくことができるのです!

Supabase

Supabaseは、オープンソースのFirebase代替として注目を集めているフルスタックバックエンドサービスです。

  • PostgreSQLデータベースを基盤とした高い拡張性と信頼性
  • リアルタイムデータベース機能でデータ変更の自動通知
  • 多様な認証方法(電子メール/パスワード、ソーシャルログイン、電話認証など)をサポート
  • ファイルストレージ機能で大容量ファイルを効率的に管理
  • RESTful APIとGraphQL APIの自動生成による高速開発
  • 開発者向けツールとSDKが豊富に用意され、設定や導入が簡単

Supabaseへの連携手順を重要視したいので、Gemini CLIで見た目の部分を作成してもらいます。これがまた泣かせるほど便利な代物!会社がGoogle Workspaceで良かった!

SupabaseとPrismaでメモ機能を作りたいです。とりあえずpage.tsxを作成してください。

おけ、じゃあ、textareaに入力した内容をSave Memoのボタンを押したらYour Memosにリストで追加するように組んで欲しい

見た目はこんな感じでOKです!入力した内容が下のリストに追加されるようになりました。ただJavaScriptでイベントしているだけなので、リロードするとリストは消えてしまいます。

入力した内容を保持するにはデータベースを使用します!

まずはSupabase側で新規プロジェクト作成をします。

「New Project」のボタンから作成、Database passwordはPrismaでコネクションする際に必要なので控えておいてください。

次にPrismaを連携させます。

npm install prisma --save-dev

npx prisma init

これらのコマンドを入力するとprismaがインストールされ、prismaフォルダが出現。

.envファイルのDATABASE_URLをSupabaseのコネクションURLに書き換えます。 [YOUR-PASSWORD]の部分に先ほどSupabaseで設定したDatabase passwordを当てはめます。

DATABASE_URL="postgresql://postgres.odoaingwnxkhkeujwhtp:[YOUR-PASSWORD]@aws-1-ap-northeast-1.pooler.supabase.com:5432/postgres"

そうしたら/prisma/schema.prisma にスキーマ(データベースの構造定義)を記述します。

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Memo {
  id        Int      @id @default(autoincrement())
  content   String
  createdAt DateTime @default(now())
}

model ○○は、任意で命名できます。

スキーマを書いたらマイグレーションをおこないます。

npx prisma migrate dev --name init

migration.sqlファイルが生成され、Supabase側にデータテーブルが作成されます。

Supabaseへ接続するためのAPI作成

入力した値をSupabaseへ保存するために接続用のAPIを作成します。

プロジェクトに Prisma Client をインストール。

npm install @prisma/client

Supabaseへの接続

/libs/prisma.ts

import { PrismaClient } from "@prisma/client"; 

const Prisma = new PrismaClient(); //インスタンス化

export const main = async () => {
  try {
    await Prisma.$connect();
  } catch (error) {
    return Error("DB接続に失敗しました");
  }
};

API

/api/memos/route.ts

import { NextResponse } from "next/server";
import { prisma } from '@/lib/prisma';

export const GET = async (req: Request, res: NextResponse) => {
  try {
    await main();             
    const memos = await Prisma.memo.findMany(); 
    return NextResponse.json(memos);
  } catch (error) {
    return NextResponse.json("エラーが発生しました");
  } finally {  
    await Prisma.$disconnect();
  }
};

export const POST = async (req: Request, res: NextResponse) => {
  const { content } = await req.json(); 
  try {
    await main();
    const memos = await Prisma.memo.**create**({
      data: {
        content: content,
      },
    });
    return NextResponse.json(memos);
  } catch (error) {
    return NextResponse.json("エラーが発生しました");
  } finally {
    await Prisma.$disconnect();
  }
};

GET関数はデータベースを全件取得するAPIで、findMany()メソッドで条件に一致する全てのレコードを取得します。Prisma.memo.findMany()の「memo」はスキーマ名です。

POST関数はデータベースに追加していくAPIで、create()で作成していきます。

他にも削除や更新などのメソッドがあり、SQL文が書けなくてもJavascriptの感覚で操作が可能です。

こちらで一通り準備はできました。

あとはpage.tsxに先ほど書いたAPIを持ってきて組み込めばOKです。

'use client';

import React, { useState, useEffect } from 'react';

interface Memo {
  id: number;
  content: string;
  createdAt: string;
}

export default function MemoApp() {
  const [memos, setMemos] = useState<Memo[]>([]);
  const [newMemo, setNewMemo] = useState('');

  const fetchMemos = async () => {
    const response = await fetch('/api/memos');
    const data = await response.json();
    setMemos(data);
  };

  useEffect(() => {
    fetchMemos();
  }, []);

  const handleSaveMemo = async (e: React.FormEvent) => {
    e.preventDefault();
    if (newMemo.trim() !== '') {
      await fetch('/api/memos', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ content: newMemo }),
      });
      setNewMemo('');
      fetchMemos();
    }
  };

  return (
    <div className="bg-white min-h-screen">
      <div className="max-w-2xl mx-auto px-4 py-16">
        <header className="text-center mb-12">
          <h1 className="text-5xl font-extrabold text-gray-900">Memo</h1>
        </header>

        <main>
          <div className="mb-12">
            <form onSubmit={handleSaveMemo}>
              <textarea
                className="w-full p-4 text-gray-800 bg-gray-100 rounded-lg border-2 border-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent transition"
                placeholder="Create a new memo..."
                rows={3}
                value={newMemo}
                onChange={(e) => setNewMemo(e.target.value)}
              ></textarea>
              <div className="flex justify-end mt-4">
                <button
                  type="submit"
                  className="px-6 py-2 bg-gray-800 text-white font-semibold rounded-lg hover:bg-gray-900 transition"
                >
                  Save Memo
                </button>
              </div>
            </form>
          </div>

          <section>
            <h2 className="text-3xl font-bold text-gray-800 mb-6">Your Memos</h2>
            <div className="space-y-4">
              {memos.length > 0 ? (
                memos.map((memo) => (
                  <div key={memo.id} className="bg-gray-50 p-6 rounded-lg shadow-sm">
                    <p className="text-gray-700">{memo.content}</p>
                  </div>
                ))
              ) : (
                <div className="text-center text-gray-500">
                  <p>No memos yet. Add one above!</p>
                </div>
              )}
            </div>
          </section>
        </main>
      </div>
    </div>
  );
}

Supabase側に保存されていますね!

今回はGeminiに頼って大まかに作成してしまいましたが、データベースを利用して値を保存できるようにすれば、作成する機能やアプリの幅も広がりますよね!

バックエンドってなんだか難しそう……と先入観がありましたが GUIツールでデータベースを管理できて分かりやすく、データベース連携のハードルが下がりました!

やったー!

この記事を書いた人

マークアップを中心に、JavaScriptやReact、Next.jsを使ってフロントエンドの開発をやっています。自分が関わったサイトが無事に公開されると嬉しかったりします!趣味はギターを弾くこと。コードは書くのも弾くのもどっちも楽しいです!

ひらっち

フロントエンドエンジニア/2022年入社

安心のチーム体制とスピードのある対応力が自慢

リベロジックでは、ベテランスタッフが積極的にプロジェクトを推進するため、お客様から高く評価されています。
プロジェクトマネージャー、ディレクターをきちんとアサインし、プロジェクト全体をスムーズに進行することを心掛けています。 不必要なフルコミットでのコスト増加を防ぎ、適材適所にリソースを配分するスタイルで、業務内容の把握から見積作成/提出の速さにも定評があります。

当社はSES的な常駐業務等は積極的に行っておりませんので予めご了承ください。

Slack、Teams、Redmine、Backlog、Asana、Jira、Notion、Google Workspace、Zoom、Webexなど、ほぼすべての主要なプロジェクト管理ツールやチャットツールをご利用いただけます。

SESやオフショアを利用する大規模案件において、技術的な課題や取り組み方にお悩みはございませんか?

ケーススタディ