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