Tutorial Laravel 12 dan Nuxt JS #10 Membuat Halaman Produk dalam Nuxt JS SSR dengan Laravel 12 API

Tutorial ini membahas integrasi lengkap antara Laravel 12 sebagai backend API dan Nuxt JS 4 sebagai frontend modern berbasis Vue 3. Cocok untuk pemula yang ingin membangun aplikasi fullstack SPA (Single Page Application) dengan REST API, dan SSR (Server Side Rendering).

✅ Telah dilihat 1355 kali

Rating: 5.00 ⭐

... 24 July 2025, 11:08

Membuat Halaman Daftar Produk pada Nuxt 4 SSR dengan Laravel 12 API

Halo teman-teman, Pada sesi kali ini, kita akan belajar bagaimana cara menampilkan daftar produk dari API Laravel ke dalam tampilan Nuxt 4 yang berjalan dalam mode SSR (Server Side Rendering).

Untuk itu, kita akan membuat sebuah halaman baru yang secara khusus akan memuat seluruh produk yang tersedia. Data produk ini akan diambil langsung dari endpoint API Laravel, lalu ditampilkan ke dalam bentuk card yang rapi dan responsif.

Setiap card produk nantinya akan menampilkan informasi dasar seperti:

  • Nama produk
  • Harga
  • Deskripsi singkat

Langkah pertama yang perlu kalian lakukan adalah membuat sebuah file baru di dalam folder pages dengan nama:

products.vue

Buka file tersebut kemudian masukkan baris kode berikut ini:

<template>
  <div class="min-h-screen bg-gradient-to-br from-blue-100 via-white to-blue-100 py-10">
    <div class="container mx-auto px-6">
      <div class="flex justify-between items-center mb-6">
        <h1 class="text-3xl font-bold text-gray-800">Daftar Produk</h1>
        <NuxtLink
          to="/product/create"
          class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-600 transition"
        >
          + Tambah Produk
        </NuxtLink>
      </div>

      <div v-if="loading" class="text-gray-500 text-center">Loading...</div>
      <div v-else-if="error" class="text-red-500 text-center">{{ error }}</div>

      <div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
        <div
          v-for="product in products"
          :key="product.id"
          class="bg-white rounded-lg shadow-md overflow-hidden transform transition duration-300 hover:scale-105 hover:shadow-lg"
        >
          <div class="p-5">
            <h2 class="text-xl font-semibold text-gray-800">{{ product.name }}</h2>
            <p class="text-gray-600 mt-1">{{ formatPrice(product.price) }}</p>
            <p class="text-gray-500 mt-2 text-sm">{{ product.description }}</p>
            <p class="text-gray-500 text-sm mt-1">
              Stok: <span class="font-medium">{{ product.stock }}</span>
            </p>

            <div class="flex justify-between mt-4">
              <button
                @click="deleteProductById(product.id)"
                class="bg-red-500 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-red-600 transition"
              >
                Hapus
              </button>
              <NuxtLink
                :to="`/product/edit/${product.id}`"
                class="bg-yellow-500 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-yellow-600 transition"
              >
                Edit
              </NuxtLink>
            </div>
          </div>
        </div>
      </div>

      <div class="flex justify-center mt-6 space-x-2">
        <button
          @click="loadProducts(meta.current_page - 1)"
          :disabled="meta.current_page === 1"
          class="px-4 py-2 rounded-lg text-sm font-medium bg-gray-300 hover:bg-gray-400 disabled:opacity-50"
        >
          ‹ Prev
        </button>

        <span class="px-4 py-2 bg-gray-200 rounded-lg">
          Page {{ meta.current_page }} of {{ meta.last_page }}
        </span>

        <button
          @click="loadProducts(meta.current_page + 1)"
          :disabled="meta.current_page === meta.last_page"
          class="px-4 py-2 rounded-lg text-sm font-medium bg-gray-300 hover:bg-gray-400 disabled:opacity-50"
        >
          Next ›
        </button>
      </div>
    </div>
  </div>
</template>



<script setup>
import { ref, onMounted } from "vue";
import { useHead } from '#imports'


const { fetchProducts, deleteProduct } = useProducts();
const products = ref([]);
const meta = ref({});
const loading = ref(false);
const error = ref(null);

const loadProducts = async (page = 1) => {
    try {
        loading.value = true;
        const response = await fetchProducts(page);
        products.value = response.data; 
        meta.value = response.meta; 
    } catch (err) {
        error.value = err.message;
    } finally {
        loading.value = false;
    }
};
useHead({
    title: "Daftar Produk | Toko Online",
    meta: [
        {
            name: "description",
            content: "Lihat daftar produk terbaik dengan harga terbaik di toko online kami.",
        },
    ],
});


const deleteProductById = async (id) => {
    if (!confirm("Apakah Anda yakin ingin menghapus produk ini?")) return;
    try {
        await deleteProduct(id);
        products.value = products.value.filter((product) => product.id !== id);
    } catch (err) {
        error.value = err.message;
    }
};

const formatPrice = (price) => {
    return new Intl.NumberFormat("id-ID", { style: "currency", currency: "IDR" }).format(price);
};

onMounted(loadProducts);
</script>

Mari kita bahas dan pelajari beberapa potongan kode diatas:

Kondisi Loading dan Error Handling

<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>

Sebelum data produk dimuat, kita tampilkan pesan Loading.... Jika terjadi kesalahan saat mem-fetch data dari API, maka pesan error akan ditampilkan.


Menampilkan Produk dalam Grid

<div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 ...">
  <div v-for="product in products" :key="product.id" class="...">

Jika data berhasil diambil, maka kita tampilkan seluruh produk dalam bentuk grid responsif. Kita menggunakan v-for untuk melakukan looping pada array products, dan setiap produk ditampilkan dalam bentuk card.

Isi dari setiap card:

  • Nama produk (product.name)
  • Harga (diformat dengan formatPrice)
  • Deskripsi dan stok
  • Tombol Edit dan Hapus

Navigasi Halaman (Pagination)

<div class="flex justify-center mt-6 space-x-2">
  <button @click="loadProducts(meta.current_page - 1)" ...>‹ Prev</button>
  <span>Page {{ meta.current_page }} of {{ meta.last_page }}</span>
  <button @click="loadProducts(meta.current_page + 1)" ...>Next ›</button>
</div>

Untuk memudahkan pengguna melihat produk dalam jumlah besar, kita tambahkan pagination sederhana. Informasi halaman aktif dan total halaman diambil dari objek meta yang dikembalikan oleh API Laravel.


Import dan Inisialisasi

import { ref, onMounted } from "vue";
import { useHead } from '#imports'

const { fetchProducts, deleteProduct } = useProducts();
  • Kita menggunakan ref dari Vue untuk mendeklarasikan variabel reaktif.
  • Fungsi useHead() digunakan untuk memberi judul dan deskripsi halaman secara dinamis (baik untuk SEO maupun user experience).
  • Composable useProducts() adalah helper yang sudah kita definisikan sebelumnya untuk melakukan fetch dan delete data produk dari API Laravel.

Fungsi loadProducts

const loadProducts = async (page = 1) => {
  ...
};

Fungsi ini bertugas mengambil data produk dari Laravel API. Secara default ia akan mengambil halaman pertama. Hasilnya akan dimasukkan ke dalam products dan meta.


Fungsi deleteProductById

const deleteProductById = async (id) => {
  ...
};

Fungsi ini dipanggil saat pengguna menekan tombol Hapus. Setelah menghapus data di server, kita juga hapus data dari array lokal products.value agar tampilan langsung diperbarui tanpa perlu reload halaman.


Fungsi formatPrice

const formatPrice = (price) => {
  return new Intl.NumberFormat("id-ID", { style: "currency", currency: "IDR" }).format(price);
};

Fungsi utilitas kecil ini berguna untuk memformat harga ke dalam format rupiah Indonesia: 175000 → Rp175.000,00


Lifecycle Hook: onMounted

onMounted(loadProducts);

Begitu halaman dimuat, fungsi loadProducts() akan langsung dipanggil untuk menampilkan produk dari server.


Npm Run Dev

Silakan teman-teman janlankan perintah npm run dev dan juga php artisan serve untuk laravel API nya. Maka berikut tampilan ketika kita akses halaman aplikasi kita pada url http://localhost:3000/products

Kesimpulan

Dengan mengikuti langkah-langkah di atas, kita telah berhasil membangun halaman daftar produk yang terintegrasi penuh dengan Laravel API. Halaman ini mendukung:

  • Tampilannya sudah responsif dan rapi
  • Dilengkapi dengan tombol edit dan hapus
  • Sudah ada pagination untuk navigasi data besar

Pada materi berikutnya, kita bisa melanjutkan dengan membuat halaman untuk input dan edit produk.

🔥 Flash Sale


📜 Table Of Contents


📌 Daftar Episode


Daftar eBook