翔和 hassy 他們正在使用 Supabase 進行一些有趣的開發項目。不同於過往的瀑布式開發方式,我聽說他們在從 UI 層面進行功能需求定義等工作,我也不能落後啊!
所以這次,我打算製作一個簡單的筆記應用,並將 Supabase 和 Prisma 整合起來,將資料保存到資料庫。我把整個步驟記錄下來供日後參考。也要用上 Gemini CLI 喔!
環境:Next.js
不使用 Supabase 的 Auth、Realtime 和 RLS,而是透過 API 利用資料庫功能,因此採用 Prisma。可以將資料庫的表格當作 JavaScript / TypeScript 的物件來處理,這樣前端工程師應該會更容易上手。
Prisma
Prisma ORM 是可在 TypeScript 或 JavaScript 中使用的 ORM(物件關聯對應)函式庫。
簡單來說,它是一個工具,允許你透過程式語言物件中定義的方法來操作資料庫,而無須撰寫 SQL。
此外,Prisma 可以從資料庫結構自動產生型別,所以當你試圖操作不存在的欄位或型別不符的資料時,編譯器就會捕捉到錯誤!
Supabase
Supabase 是一個開源的 Firebase 替代方案,作為全棧後端服務備受關注。
- 以 PostgreSQL 資料庫為基礎,具有高度的擴展性和可靠性
- 透過即時資料庫功能自動通知資料變更
- 支援多種驗證方式(電子郵件/密碼、社群登入、電話驗證等)
- 透過檔案儲存功能有效管理大容量檔案
- 自動生成 RESTful API 和 GraphQL API,加快開發速度
- 提供豐富的開發者工具和 SDK,設定和導入簡單
我想重視 Supabase 的連接步驟,所以請用 Gemini CLI 來建立前端部分。這真是好用得令人欣喜的工具!公司使用 Google Workspace 真是太好了!
我想用 Supabase 和 Prisma 建立備忘錄功能。先請建立 page.tsx。
沒問題,那麼請幫我設計成在 textarea 輸入內容後,按下「Save Memo」按鈕就能將其作為清單項目新增到「Your Memos」中。
外觀就這樣沒問題!輸入的內容會被添加到下面的清單中。但由於只是用 JavaScript 處理事件,重新整理頁面後清單就會消失。
要保持輸入的內容,需要使用資料庫!
首先在 Supabase 端建立新專案。
從「New Project」按鈕進行建立,資料庫密碼是後續用 Prisma 連線時所需的,請妥善保管。
接下來連結 Prisma。
npm install prisma --save-dev
npx prisma init
輸入這些命令後,Prisma 就會被安裝,並出現 prisma 資料夾。
將 .env 檔案中的 DATABASE_URL 改為 Supabase 的連線 URL。將 [YOUR-PASSWORD] 部分替換為剛才在 Supabase 設定的資料庫密碼。
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 的方式進行操作。
到這裡為止,準備工作都已完成。
接下來只要將先前撰寫的 API 帶入 page.tsx 中進行整合即可。
'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 的前端開發。看到自己參與的網站順利上線時最開心!興趣是彈吉他。喜歡貓咪和烤地瓜🐱🍠
Hiracchi
前端工程師 / 2022年入職