Cara Membuat Crud Product Menggunakan Next Js Mysql dan Prisma
Membuat Aplikasi POS Sederhana dengan Next.js, Prisma & MySQL — Tanpa Ribet, Tanpa API Routes!
Jika kamu ingin membuat aplikasi Point of Sale (POS) modern, cepat, dan mudah dikelola — tutorial ini cocok untukmu! Kita akan gunakan Next.js App Router + Prisma ORM + MySQL, dengan pendekatan Server Actions — artinya, tidak perlu bikin API endpoint sama sekali. Simpel, aman, dan powerful!
Struktur project yang kita bangun:
/src
/app
/products
page.tsx ← Halaman daftar produk
/new
page.tsx ← Halaman tambah produk
/[id]/edit
page.tsx ← Halaman edit produk
/lib
/actions
product.ts ← Server Actions utama (CRUD)
prisma.ts ← Koneksi database singleton
/prisma
schema.prisma ← Model database

🔧 Langkah 1: Setup Awal Project & Database
Pastikan kamu sudah install Node.js, lalu buat project baru:
npx create-next-app@latest pos-sederhana
cd pos-sederhana
npm install prisma @prisma/client
npx prisma init
Edit file .env di root project, sesuaikan dengan koneksi MySQL-mu:
DATABASE_URL="mysql://root@localhost:3306/pos_db"
Lalu, definisikan model produk di prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Product {
id Int @id @default(autoincrement())
nama String
harga Float
stok Float
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Push model ke database:
npx prisma db push
🔌 Langkah 2: Setup Prisma Client (Singleton)
Buat file src/lib/prisma.ts — ini akan jadi satu-satunya instance koneksi ke database:
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client'
declare global {
var prisma: PrismaClient | undefined
}
const prisma = globalThis.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma
export default prisma
⚡ Langkah 3: Buat Server Actions (Inti dari Aplikasi)
Buat folder src/lib/actions/, lalu buat file product.ts. Ini adalah otak dari aplikasi kita — semua operasi CRUD ada di sini:
// src/lib/actions/product.ts
"use server"
import prisma from "@/lib/prisma"
import { revalidatePath } from "next/cache"
// Ambil semua produk
export async function getProducts() {
return await prisma.product.findMany({
orderBy: { id: "asc" },
})
}
// Tambah produk baru
export async function createProduct(data: { nama: string; harga: number; stok: number }) {
await prisma.product.create({ data })
revalidatePath("/products") // refresh halaman setelah tambah
}
// Edit produk
export async function updateProduct(id: number, data: { nama: string; harga: number; stok: number }) {
await prisma.product.update({
where: { id },
data,
})
revalidatePath("/products") // refresh halaman setelah edit
}
// Hapus produk
export async function deleteProduct(id: number) {
await prisma.product.delete({
where: { id },
})
revalidatePath("/products") // refresh halaman setelah hapus
}
// Ambil produk berdasarkan ID (untuk halaman edit)
export async function getProductById(id: number) {
const product = await prisma.product.findUnique({
where: { id },
})
if (!product) throw new Error("Produk tidak ditemukan")
return product
}
📋 Langkah 4: Halaman Daftar Produk (src/app/products/page.tsx)
Halaman utama yang menampilkan semua produk dalam bentuk tabel, lengkap dengan tombol edit dan hapus:
// src/app/products/page.tsx
// src/app/products/page.tsx
import { getProducts } from "../../lib/actions/product"
import Link from "next/link"
export default async function ProductsPage() {
const products = await getProducts()
return (
<div className="p-6 max-w-4xl mx-auto">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Daftar Produk</h1>
<Link
href="/products/new"
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
+ Tambah Produk
</Link>
</div>
<div className="border rounded-md">
<table className="w-full">
<thead>
<tr className="bg-gray-100">
<th className="text-left p-3">Nama</th>
<th className="text-right p-3">Harga</th>
<th className="text-right p-3">Stok</th>
<th className="text-center p-3">Aksi</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr key={product.id} className="border-b">
<td className="p-3">{product.nama}</td>
<td className="p-3 text-right">Rp{product.harga.toLocaleString()}</td>
<td className="p-3 text-right">{product.stok}</td>
<td className="p-3 text-center">
<a
href={`/products/${product.id}/edit`}
className="text-blue-600 hover:underline mr-3"
>
Edit
</a>
<button
formAction={async () => {
'use server'
await deleteProduct(product.id)
}}
className="text-red-600 hover:underline"
>
Hapus
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
// Import fungsi delete di sini (karena dipakai di formAction)
import { deleteProduct } from "@/lib/actions/product"
➕ Langkah 5: Halaman Tambah Produk (src/app/products/new/page.tsx)
Form sederhana untuk menambahkan produk baru — sudah dilengkapi validasi anti-NaN:
// src/app/products/new/page.tsx
import { redirect } from "next/navigation"
export default function NewProductPage() {
async function createProduct(formData: FormData) {
'use server'
// Ambil data dari form
const nama = formData.get('nama') as string
const hargaRaw = formData.get('harga') as string
const stokRaw = formData.get('stok') as string
// Validasi & konversi aman
const harga = parseFloat(hargaRaw)
const stok = parseFloat(stokRaw)
// Jika NaN, ganti dengan 0
const hargaValid = isNaN(harga) ? 0 : harga
const stokValid = isNaN(stok) ? 0 : stok
// Jika nama kosong, bisa kasih error (opsional)
if (!nama || nama.trim() === '') {
throw new Error('Nama produk wajib diisi')
}
// Import fungsi createProduct
const { createProduct } = await import('@/lib/actions/product')
await createProduct({
nama: nama.trim(),
harga: hargaValid,
stok: stokValid
})
redirect('/products')
}
return (
<div className="p-6 max-w-lg mx-auto">
<h1 className="text-2xl font-bold mb-6">Tambah Produk Baru</h1>
<form action={createProduct} className="space-y-4">
<div>
<label className="block mb-1 font-medium">Nama Produk</label>
<input
type="text"
name="nama"
required
className="w-full p-2 border rounded"
placeholder="Contoh: Kopi Susu"
/>
</div>
<div>
<label className="block mb-1 font-medium">Harga</label>
<input
type="number"
name="harga"
step="0.01"
min="0"
required
className="w-full p-2 border rounded"
placeholder="Contoh: 15000"
defaultValue="0"
/>
</div>
<div>
<label className="block mb-1 font-medium">Stok</label>
<input
type="number"
name="stok"
step="0.01"
min="0"
required
className="w-full p-2 border rounded"
placeholder="Contoh: 10"
defaultValue="0"
/>
</div>
<div className="pt-4 flex gap-2">
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Simpan Produk
</button>
<a
href="/products"
className="border px-4 py-2 rounded hover:bg-gray-50"
>
Batal
</a>
</div>
</form>
</div>
)
}
✏️ Langkah 6: Halaman Edit Produk (src/app/products/[id]/edit/page.tsx)
Halaman untuk mengedit produk yang sudah ada — menggunakan dynamic route [id]:
// src/app/products/[id]/edit/page.tsx
import { getProductById } from "@/lib/actions/product"
import { updateProduct } from "@/lib/actions/product"
import { redirect } from "next/navigation"
export default async function EditProductPage({ params }: { params: { id: string } }) {
const id = parseInt(params.id)
const product = await getProductById(id)
// Server Action Wrapper — ini solusi utamanya
async function updateProductAction(formData: FormData) {
'use server'
const nama = formData.get('nama') as string
const harga = parseFloat(formData.get('harga') as string)
const stok = parseFloat(formData.get('stok') as string)
await updateProduct(id, { nama, harga, stok })
redirect('/products') // kembali ke daftar setelah sukses
}
return (
<div className="p-6 max-w-lg mx-auto">
<h1 className="text-2xl font-bold mb-6">Edit Produk</h1>
<form action={updateProductAction} className="space-y-4">
<div>
<label className="block mb-1 font-medium">Nama Produk</label>
<input
type="text"
name="nama"
defaultValue={product.nama}
required
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block mb-1 font-medium">Harga</label>
<input
type="number"
name="harga"
step="0.01"
min="0"
defaultValue={product.harga}
required
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block mb-1 font-medium">Stok</label>
<input
type="number"
name="stok"
step="0.01"
min="0"
defaultValue={product.stok}
required
className="w-full p-2 border rounded"
/>
</div>
<div className="pt-4 flex gap-2">
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Update Produk
</button>
<a
href="/products"
className="border px-4 py-2 rounded hover:bg-gray-50"
>
Batal
</a>
</div>
</form>
</div>
)
}
Cara Menjalankan Aplikasi
Setelah semua file di atas dibuat, jalankan perintah:
npm run dev
Lalu buka browser dan akses:
http://localhost:3000/products
Kamu akan melihat daftar produk, dan bisa menambah, mengedit, atau menghapus produk secara langsung — tanpa bikin satu pun file API!
💡 Keunggulan Pendekatan Ini:
- ✅ Tanpa API Routes — lebih simpel dan aman
- ✅ Server Actions — data langsung diproses di server, tidak bocor ke client
- ✅ Auto-refresh — halaman otomatis update setelah operasi CRUD
- ✅ Anti Error NaN — sudah ditangani di form handler
- ✅ Mudah Dikembangkan — bisa ditambah fitur transaksi, laporan, dll
🔥 Siap untuk tahap selanjutnya? Misalnya menambahkan fitur transaksi penjualan, cetak struk, atau manajemen pelanggan? Tinggalkan komentar di bawah!
Artikel lain yang berkaitan:
Komentar
Posting Komentar