PART 2 — Implementasi API pada Frontend

Pada Part 1 kita telah menyelesaikan pembuatan struktur proyek Next.js, komponen-komponen dasar, hingga halaman Home, Produk, Produk Detail, dan Keranjang. Kini, kita akan melanjutkan ke tahap berikutnya yaitu menghubungkan aplikasi frontend dengan API backend.

Agar data produk yang tampil di aplikasi tidak lagi berupa dummy data, kita akan menyiapkannya langsung dari server menggunakan API sederhana yang telah disediakan.


  • Backend yang Digunakan

Dalam tutorial ini, backend sederhana sudah disiapkan dan dapat diakses pada dua tempat:
Google Drive
(file sudah tersedia untuk di-download)
Link:
https://drive.google.com/drive/folders/1mgpptu94FtG5s_hXt3E4vm9HpQ3OHbf9?usp=drive_link
Repository GitHub
Link:
https://github.com/kadekwidiana/simple-ecommerce-be.git

  • Setup dan Menjalankan Backend di Komputer Lokal

Untuk menjalankan backend secara lokal, silakan mengikuti dokumentasi yang ada pada repository GitHub.

Dokumentasi lengkap sudah tersedia di dalam repository dan juga di google drive, sehingga Anda hanya perlu mengikuti langkah-langkahnya, seperti:

  • Instalasi dependency
  • Setup environment
  • Menjalankan server
  • Import ke postman


  • Import Dokumentasi API ke Postman

Agar proses pengujian API lebih mudah, backend sudah menyediakan file dokumentasi API dalam format Postman Collection.

Langkah-langkahnya sebagai berikut:

Buka Folder Dokumentasi API
Masuk ke folder: simple-ecommerce-be/docs/Simple Ecommerce.postman_collection seperti pada Gambar 1 berikut:
Gambar 1



Buka Aplikasi Postman pada lokal komputer masing-masing (pastikan sudah tersinstall, jika belum bisa menggunakan Postman Web)

Import File Postman Collection
Klik Import → pilih file: Simple Ecommerce.postman_collection.json seperti pada Gambar 2 berikut:
Gambar 2

Jika sudah berhasil, collection API akan muncul di sidebar Postman seperti pada Gambar 3 berikut:
Gambar 3


  • Membuat Enviroment di Postman 

Agar API mudah diakses dan base URL bisa diganti-ganti, kita membuat sebuah environment.

Langkah-langkah:
  1. Klik Environments
  2. Tambahkan environment baru
  3. Isi variabel BASE_URL = http://localhost:8000
Gambar 4

Pilih Environment, pastikan environment yang sudah dibuat dipilih/digunakan, seperti pada Gambar 5 berikut:
Gambar 5


  •  Coba Test API Produk (GET /products/list)

Sekarang kita coba salah satu endpoint terpenting yaitu GET products untuk mendapatkan daftar produk dari backend.
Pilih: Products → List
Klik Send.
Jika berhasil, hasilnya kurang lebih seperti Gambar 6 berikut:
Gambar 6

Anda akan melihat data produk tampil dalam bentuk JSON, menandakan bahwa backend sudah berjalan dengan baik dan siap diintegrasikan ke frontend Next.js.


  • Skema Implementasi API pada Next.js

Tadi kita sudah berhasil untuk setup Backend di lokal komputer kita, sekarang kita akan lanjutkan ke implementasi di Frontend nya.
Pada bagian backend (BE), kita menggunakan Laravel yang menyediakan RESTful API sederhana. Sementara di bagian frontend (FE), arsitektur dibagi menjadi beberapa layer agar lebih terstruktur, mudah dipelihara, dan memiliki pemisahan tanggung jawab yang jelas. Layer tersebut adalah: Services, Features, Client Components, dan Server Components. Gambaran skema nya bisa dilihat pada Gambar 7 berikut:
Gambar 7

Secara umum, Services dan Server Components berjalan dengan SSR (Server-Side Rendering), sedangkan Features dan Client Components berjalan dengan CSR (Client-Side Rendering).

  • Backend (Laravel RESTful API)
Pada sisi backend, Laravel menyediakan endpoint RESTful seperti: get products dan categories. API ini akan menjadi sumber data utama untuk aplikasi Next.js.
  • Services (SSR)

Layer Services adalah bagian yang berfungsi sebagai jembatan antara frontend dan backend. Di sinilah integrasi API terjadi melalui function seperti: getProducts(), getProductDetail(id), dan lainnya.

Alasan utama API dipanggil dari layer Services menggunakan SSR:

    • Keamanan Data
Jika fetch API dilakukan langsung di Client Component, maka:
      • URL endpoint terlihat di browser
      • Token atau API key bisa terekspos
      • Request bisa dengan mudah di-inspect atau manipulate
Dengan memindahkan fetch ke SSR:
      • Endpoint API tidak terlihat di client
      • Token tidak terekspos
      • Permintaan terjadi di server, sehingga lebih aman
      • Dapat menghindari masalah CORS origin
    • Kinerja yang Lebih Baik
Data diproses di server sebelum sampai ke browser, sehingga page load lebih cepat.
  • Features (CSR)
Layer Features berisi berbagai custom hook untuk mengambil data dari Services, contohnya: useGetProducts(), useGetProductDetail(), dan lainnya.
Hook ini berfungsi sebagai wrapper atau adapter yang membuat data dari services mudah digunakan oleh Client Component.
Dengan memisahkan Features:
    • Alur data menjadi lebih bersih
    • Logic fetch di SSR tetap aman
    • Component tidak perlu mengetahui detail API
  •  Client Components (CSR)
Bagian ini berisi komponen yang membutuhkan interaksi dengan user, misalnya: Button, Card, Section, dan komponen lainnya.
Client Component menggunakan CSR karena:
    • Membutuhkan interaktivitas
    • Menggunakan event seperti onClick, onChange
    • Berhubungan dengan state management, dan lainnya
Client Component tidak memanggil API langsung, tetapi mengambil data dari layer Features.
  • Server Components (SSR)
Server Component berisi: page, layout, dan komponen yang tidak membutuhkan interaktivitas
Mengapa menggunakan Server Component?
    • Lebih cepat (data diproses di server)
    • SEO lebih baik (konten sudah siap sebelum dikirim ke browser)
    • Keamanan lebih tinggi
    • Tidak membebani browser dengan JavaScript tambahan
Data dari Services bisa disalurkan ke Server Component, kemudian diteruskan ke Client Component bila dibutuhkan.

Alur kerja secara umum
    • Server Components memanggil Services jika membutuhkan data dinamis dari backend
    • Services melakukan fetch ke RESTful API Laravel
    • Data disalurkan ke Features melalui custom hook
    • Features digunakan oleh Client Components
    • Client Components melakukan interaksi dengan user
    • Jika ada perubahan yang butuh API, permintaan akan kembali melalui Services (SSR)
Kesimpulan
Arsitektur ini memberikan beberapa manfaat utama:
    • Struktur rapi & mudah di-maintain
    • Keamanan lebih baik (endpoint dan token tersimpan di server)
    • Performanya tinggi berkat SSR dan pemisahan komponen
    • Maksimal untuk SEO
    • Mudah dikembangkan karena setiap layer punya fungsi yang jelas
Dengan mengikuti pola ini, aplikasi Next.js menjadi lebih modular, scalable, dan aman saat terhubung dengan RESTful API dari backend seperti Laravel.

  • Langkah implementasi API di Next.js

    • Membuat file environment

Buat file .env di root project FE
Masukan kode berikut:
NEXT_PUBLIC_API_URL=http://localhost:8000/api
NEXT_PUBLIC_API_URL berfungsi sebagai base URL API yang akan digunakan di seluruh aplikasi. Jika sudah dibuat kode nya akan seperti Gambar 8 berikut:
Gambar 8

    • Membuat Folder services/

Di dalam folder src, buat folder baru bernama services.

Strukturnya akan seperti Gambar 9 berikut:

Gambar 9

Dalam folder services buat file dengan nama products.services.js

kemudian masukan kode berikut:

'use server';
import axios from 'axios';

export const getProductList = async ({ page, perpage, name, category_id }) => {
    try {
        const res = await axios({
            method: 'GET',
            url: `${process.env.NEXT_PUBLIC_API_URL}/api/products/list`,
            params: {
                page,
                perpage,
                name,
                category_id
            },
            headers: {
                'Content-Type': 'application/json',
            },
        });
        return res.data;
    } catch (error) {
        throw new Error(
            error?.response?.data?.message ||
            'Terjadi kesalahan saat mengambil data produk.'
        );
    }
};

export const getProductById = async (productId) => {
    try {
        const res = await axios({
            method: 'GET',
            url: `${process.env.NEXT_PUBLIC_API_URL}/api/products/${productId}`,
            headers: {
                'Content-Type': 'application/json',
            },
        });
        return res.data;
    } catch (error) {
        throw new Error(
            error?.response?.data?.message ||
            'Terjadi kesalahan saat mengambil data produk.'
        );
    }
};

Penjelasan singkat
Terdapat "use server" → menandakan fungsi ini diproses di Server Side Rendering (SSR).
Axios digunakan untuk request API.
Tersedia 2 fungsi:
getProductList() → mengambil daftar produk.
getProductById() → mengambil detail produk.
Dengan memanggil API di server:
- Endpoint API tidak terlihat jelas di browser.
- Menghindari potensi kebocoran token (jika ada api yang menggunakan token).
- Tidak terkena isu CORS.

    • Membuat Folder features/
Selanjutnya buat folder features dalam src.
Di dalamnya buat folder product, lalu buat dua file: src/features/product/useGetProductList.js dan src/features/product/useGetProductById.js
Struktur folder akan seperti Gambar 10 berikut:

Gambar 10

Masukan kode berikut pada file useGetProductList.js:

import { getProductList } from '@/services/products.service';
import { useEffect, useState } from 'react';

export default function useGetProductList({
    page = 1,
    perpage = 10,
    name = '',
    category_id = '',
}) {
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(null);
    const [productList, setProductList] = useState([]);
    const [pagination, setPagination] = useState({});

    const fetchData = async () => {
        setIsLoading(true);
        setError(null);

        try {
            const res = await getProductList({ page, perpage, name, category_id });

            setProductList(res.data);
            setPagination(res.pagination);
        } catch (error) {
            console.error(error);
            setError(error.message);
        } finally {
            setIsLoading(false);
        }
    };

    useEffect(() => {
        fetchData();
    }, [page, perpage, name, category_id]);

    return {
        isLoading,
        error,
        productList,
        pagination,
        fetchData
    };
}
Kode pada file useGetProductList.js merupakan function untuk get list data produk, didalam kode tersebut ada state yang menyimpan seperti loading, error, produk list, dan pagination, serta juga menggunakan hook dari react yaitu useState dan useEffect untuk proses fetch data, kemudian kode ini akan bisa di gunakan di component yang memerlukan/membutuhkan.

Masukan kode berikut pada useGetProductById.js:
import { getProductById } from '@/services/products.service';
import { useEffect, useState } from 'react';

export default function useGetProductById(productId) {
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(null);
    const [product, setProduct] = useState({});

    const fetchData = async () => {
        setIsLoading(true);
        setError(null);

        try {
            const res = await getProductById(productId);

            setProduct(res.data);
        } catch (error) {
            setError(error.message);
        } finally {
            setIsLoading(false);
        }
    };

    useEffect(() => {
        fetchData();
    }, []);

    return { isLoading, error, product };
}
Kode pada file useGetProductById.js merupakan function untuk get data produk detail, didalam kode tersebut ada state yang menyimpan seperti loading, error, dan produk, serta juga menggunakan hook dari react yaitu useState dan useEffect untuk proses fetch data, kemudian kode ini akan bisa di gunakan di component yang memerlukan/membutuhkan.

    • Menggunakan Features pada Client Component
Buka file src/components/pages/product/ProductPage.jsx, kemudian ubah kode nya menjadi seperti berikut:
"use client";
import FrontpageLayout from "@/components/layouts/FrontpageLayout";
import ProductCard from "@/components/ui/card/ProductCard";
import FetchError from "@/components/ui/error/FetchError";
import SpinnerLoading from "@/components/ui/loading/SpinnerLoading";
import useGetProductList from "@/features/product/useGetProductList";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";

export default function ProductPageContent() {
  const searchParams = useSearchParams();
  const router = useRouter();

  const [search, setSearch] = useState(searchParams.get("name") || "");

  const { isLoading, error, productList } = useGetProductList({
    perpage: 12,
    name: searchParams.get("name"),
  });

  const handleSearch = (e) => {
    e.preventDefault();
    if (search === "") {
      router.push(`/products`);
      return;
    }

    router.push(`/products?name=${search}`);
  };

  return (
    <FrontpageLayout>
      <div className="flex flex-col gap-4 w-full px-4">
        <div className="flex justify-between items-center gap-2">
          <h3 className="font-bold text-gray-800 text-3xl text-start mb-4">
            Produk
          </h3>

          <form className="w-1/2 sm:w-1/3" onSubmit={handleSearch}>
            <label
              htmlFor="default-search"
              className="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white"
            >
              Search
            </label>
            <div className="relative">
              <input
                type="search"
                id="default-search"
                className="block w-full p-4 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                placeholder="Cari produk..."
                value={search}
                onChange={(e) => setSearch(e.target.value)}
              />
              <button
                type="submit"
                className="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
              >
                Cari
              </button>
            </div>
          </form>
        </div>

        {isLoading && <SpinnerLoading />}

        {error && <FetchError text={error} />}

        {!isLoading && !error && productList.length === 0 && (
          <FetchError text={"Produk tidak ditemukan"} />
        )}

        {!isLoading && !error && productList.length > 0 && (
          <>
            <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
              {productList.map((product, index) => (
                <ProductCard key={index} product={product} />
              ))}
            </div>
          </>
        )}
      </div>
    </FrontpageLayout>
  );
}

Nahh, disini kita sudah menerapkan custom hook fetch data dari features nya, dalam file ProductPage.jsx tersebut kita tinggal memanggil features yang sudah kita buat, kemudian menambahkan beberapa state seperti fitur pencarian, conditional rendering jika loading, error, dan sudah ada data. jika sudah berhasil bisa di cek di http://localhost/products, maka tampilan nya seperti pada Gambar 11 berikut:
Gambar 11

Selanjutkan jika akan implementasikan pada produk detail
buka file src/components/pages/product/ProductDetailPage.jsx, kemudian ubah kode nya menjadi seperti berikut:
"use client";
import FrontpageLayout from "@/components/layouts/FrontpageLayout";
import Button from "@/components/ui/button/Button";
import FetchError from "@/components/ui/error/FetchError";
import SpinnerLoading from "@/components/ui/loading/SpinnerLoading";
import useGetProductById from "@/features/product/useGetProductById";
import { useParams } from "next/navigation";
import { FaBagShopping } from "react-icons/fa6";

export default function ProductDetailPageContent() {
  const { productId } = useParams();

  const { isLoading, error, product } = useGetProductById(productId);

  return (
    <FrontpageLayout>
      <h3 className="font-bold text-gray-800 text-3xl text-start px-4 mb-4">
        Detail Produk
      </h3>
      {isLoading && <SpinnerLoading />}
      {error && <FetchError text={error} />}
      {!isLoading && !error && product && (
        <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4">
          <img src={product.image} alt="product image" className="rounded-sm" />
          <ul className="flex flex-col gap-4">
            <li className="text-3xl font-semibold">{product.name}</li>{" "}
            <li>Kategori: {product?.category?.name}</li>{" "}
            <li>
              {" "}
              Harga:{" "}
              {product.price
                ? new Intl.NumberFormat("id-ID", {
                    style: "currency",
                    currency: "IDR",
                    minimumFractionDigits: 0,
                  }).format(parseFloat(product.price))
                : "Rp 0"}{" "}
            </li>
            <li>Stok: {product.stock}</li>{" "}
            <li>Deskripsi: {product.description}</li>{" "}
            <li>
              <Button
                className={`text-xs ${product.stock === 0 && "opacity-50"}`}
                disabled={product.stock === 0}
              >
                <FaBagShopping className="size-4" />
                Tambah ke Keranjang
              </Button>
            </li>
          </ul>
        </div>
      )}
    </FrontpageLayout>
  );
}
Nahh, disini kita sudah menerapkan custom hook fetch data dari features nya, dalam file ProductDetailPage.jsx tersebut kita tinggal memanggil features yang sudah kita buat, kemudian menambahkan beberapa state seperti use param untuk mendapatkan id produk nya, conditional rendering jika loading, error, dan sudah ada data. jika sudah berhasil bisa di cek di http://localhost/products/1, maka tampilan nya seperti pada Gambar 12 berikut:
Gambar 12


Hore!🎉
Kita sudah berhasil untuk implementasi API di Frontend Next.js dengan menggunakan Axios, dan pola arsitektur yang mudah di maintainance

Sampai jumpa di PART 3, di mana kita akan mulai implementasi state management untuk kelola keranjang belanja dengan menggunakan Zustand