Shō and Hashie, among others, are using Supabase to build some interesting new projects. Unlike the traditional waterfall approach, they're defining functional requirements starting from the UI, and I keep hearing bits and pieces about it. I don't want to fall behind!
So this time, I'll build a simple memo app and write up my notes on how to connect Supabase and Prisma to save data to a database. Gemini CLI is going to be part of this too!
Environment: Next.js
Since we won't be using Supabase's Auth, Realtime, or RLS, and will instead access the database features through an API, we're adopting Prisma. Because it lets you work with database tables like JavaScript or TypeScript objects, it should feel familiar to frontend engineers.
Prisma
Prisma ORM is an ORM (Object-Relational Mapping) library that can be used with TypeScript and JavaScript.
Put simply, it's a tool that lets you operate on a database without writing SQL, using methods defined on programming language objects.
Prisma also automatically generates types from the database schema, so if you try to manipulate a column that doesn't exist or data with a mismatched type, you'll catch it as a compile error!
Supabase
Supabase is a full-stack backend service gaining attention as an open-source alternative to Firebase.
- High scalability and reliability built on a PostgreSQL database
- Automatic notifications of data changes through real-time database features
- Supports a wide range of authentication methods, including email/password, social login, and phone authentication
- Efficiently manages large files with file storage features
- Fast development with automatically generated RESTful and GraphQL APIs
- Rich developer tools and SDKs make setup and adoption simple
Since I want to focus on the steps for connecting to Supabase, I'll have Gemini CLI build the visual parts. It's incredibly handy, almost moving! Good thing our company uses Google Workspace!
I want to build a memo feature with Supabase and Prisma. For now, please create a page.tsx.
Got it. Then please set it up so that when the content entered in the textarea is submitted by clicking the Save Memo button, it gets added as a list under Your Memos.
The layout looks good! Entered content is now added to the list below. However, since this is just handled by a JavaScript event, the list will disappear when the page is reloaded.
To keep the entered content, we'll use a database!
First, create a new project on the Supabase side.
Create it using the "New Project" button. Please make a note of the Database password, as it is required when connecting with Prisma.
Next, we'll integrate Prisma.
npm install prisma --save-dev
npx prisma init
Running these commands will install Prisma and create a prisma folder.
Replace DATABASE_URL in the .env file with the Supabase connection URL. Replace the [YOUR-PASSWORD] part with the Database password you set earlier in Supabase.
DATABASE_URL="postgresql://postgres.odoaingwnxkhkeujwhtp:[YOUR-PASSWORD]@aws-1-ap-northeast-1.pooler.supabase.com:5432/postgres"
Next, write the schema (the definition of the database structure) in /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())
}
You can name the model ○○ as you like.
Once the schema is written, run the migration.
npx prisma migrate dev --name init
A migration.sql file is generated, and the data table is created on the Supabase side.
Creating an API to connect to Supabase
To save the entered values to Supabase, we create an API for the connection.
Install Prisma Client in the project.
npm install @prisma/client
Connecting to 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();
}
};
The GET function is an API that retrieves all records from the database. The findMany() method fetches all records that match the specified conditions. In Prisma.memo.findMany(), "memo" is the schema name.
The POST function is an API that adds records to the database, using create() to do so.
There are also methods for deleting and updating, so you can work with the data in a JavaScript-like way even if you can't write SQL.
That covers the basic setup.
Next, simply copy the API you wrote earlier into page.tsx and integrate it, and you're all set.
'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>
);
}
It's saved on the Supabase side, right!
This time I relied on Gemini to put together a rough version, but if we can save values using a database, the range of features and apps we can build will expand too!
I had a preconception that backends would be difficult... but being able to manage the database with a GUI tool made it easy to understand, and the barrier to connecting to a database feels much lower!
Yay!
I work mainly on markup and frontend development using JavaScript, React, and Next.js. I'm always happy when a site I've been involved in launches successfully! My hobbies are playing the guitar. I love cats and roasted sweet potatoes 🐱🍠
Hirocchi
Frontend Engineer / Joined in 2022