Jasa Setting Mikrotik

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
cara membuat crud nextjs


🔧 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:

  1. Apa Itu Next.js? Definisi dan Posisinya dalam Ekosistem Web

Komentar