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
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:
Masuk ke folder: simple-ecommerce-be/docs/Simple Ecommerce.postman_collection seperti pada Gambar 1 berikut:
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:
-
Membuat Enviroment di Postman
Agar API mudah diakses dan base URL bisa diganti-ganti, kita membuat sebuah environment.
- Klik Environments
- Tambahkan environment baru
-
Isi variabel BASE_URL =
http://localhost:8000
|
|
| Gambar 4 |
- Coba Test API Produk (GET /products/list)
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
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:
- Backend (Laravel RESTful API)
- 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
- URL endpoint terlihat di browser
- Token atau API key bisa terekspos
- Request bisa dengan mudah di-inspect atau manipulate
- 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
- Features (CSR)
- Alur data menjadi lebih bersih
- Logic fetch di SSR tetap aman
- Component tidak perlu mengetahui detail API
- Client Components (CSR)
- Membutuhkan interaktivitas
- Menggunakan event seperti onClick, onChange
- Berhubungan dengan state management, dan lainnya
- Server Components (SSR)
- 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
- 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)
- 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
- Langkah implementasi API di Next.js
- Membuat file environment
- Membuat file environment
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: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.'
);
}
};"use server" → menandakan fungsi ini diproses di Server Side Rendering (SSR).getProductList() → mengambil daftar produk.getProductById() → mengambil detail produk.- Membuat Folder
features/
features dalam src.product, lalu buat dua file: src/features/product/useGetProductList.js dan src/features/product/useGetProductById.js| 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
};
}
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 };
}
- Menggunakan Features pada Client Component
"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>
);
}
| Gambar 11 |
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>
);
}
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

0 Comments
Posting Komentar