T</>TahuCoding

Part 5 - Tutorial Next.js 13 Typescript Ecommerce Cart dengan Payment Gateway - Product Datatables Server Side

Loading...

Pada tutorial ini kita belajar bagaimana caranya mendapatkan data dari table product menggunakan api hingga integrasi ke sisi UI menggunakan datatable dan RTK Query.

Step 1: Membuat API GET Products

Buatlah sebuah file route.ts pada folder api/dashboard/products, file ini berguna untuk GET data products yang ada di database secara pagination:

src\app\api\dashboard\products\route.ts
import { NextResponse, NextRequest } from "next/server";
import prisma from "@/lib/prisma";
 
const { json: jsonResponse } = NextResponse;
 
export const GET = async (request: NextRequest) => {
  const url = new URL(request.url);
 
  // Current page (default: 1)
  const page = parseInt(url.searchParams.get("page") as string) || 1;
 
  // Number of items per page (default: 10)
  const limit = parseInt(url.searchParams.get("limit") as string) || 10;
 
  try {
    // Get total count of products
    const totalCount = await prisma.product.count();
 
    // Calculate total number of pages
    const totalPages = Math.ceil(totalCount / limit);
 
    const products = await prisma.product.findMany({
      skip: (page - 1) * limit, // Calculate the number of items to skip based on the current page and limit
      take: limit, // Set the number of items to retrieve per page
      include: {
        category: true,
      },
    });
 
    return jsonResponse(
      {
        message: "Successfully get products",
        data: {
          products,
          totalCount,
          totalPages,
        },
      },
      {
        status: 200,
      }
    );
  } catch (_error) {
    return jsonResponse(
      {
        message: "Server Error",
      },
      {
        status: 500, //server error status code
      }
    );
  }
};

Berikut penjelasan dari Code diatas:

Pada awalnya, kita membuat objek URL baru menggunakan new URL(request.url). Kemudian, kita mengambil value parameter "page" dari query string URL menggunakan url.searchParams.get("page"). Jika value tersebut tidak ada atau tidak valid, maka value defaultnya adalah 1.

Kemudian, kita mengambil value parameter "limit" dari query string URL menggunakan url.searchParams.get("limit"). Jika value tersebut tidak ada atau tidak valid, maka value defaultnya adalah 10.

Setelah itu, kita menghitung total jumlah data produk yang ada menggunakan prisma.product.count(). Hal ini berguna untuk menghitung total page yang tersedia.

Kemudian, kita menghitung total jumlah page dengan membagi total jumlah data dengan batas jumlah data per page yang ditentukan.

Selanjutnya, kita menggunakan method prisma.product.findMany() untuk mengambil data produk dengan memanfaatkan parameter skip dan take untuk mengatur page dan batas jumlah data yang diambil per page. Kita juga menggunakan include untuk memasukkan kolom terkait seperti kategori.

Terakhir, kita mengembalikan respons JSON yang berisi data produk, total jumlah data, dan total jumlah page.

Kamu dapat mengakses url ini menggunakan endpoint:

http://localhost:3000/api/dashboard/products

atau

http://localhost:3000/api/dashboard/products?page=1&limit=5

Step 2: Menambahkan Product Services

Pada kesempatan ini kita akan menggunakan RTK Query yang merupakan library yang disediakan oleh Redux Toolkit untuk memudahkan pengelolaan state dan pemanggilan API dalam aplikasi React. Dengan RTK Query, kita dapat dengan mudah melakukan fetching data, caching, invalidation, dan update data dari server. Library ini menyediakan hooks yang otomatis mengatur pemanggilan API dan menyimpan data dalam state Redux secara efisien. Dengan menggunakan RTK Query, developer dapat mengurangi boilerplate code dan meningkatkan produktivitas dalam mengelola data aplikasi.

Buatlah folder service lalu tambahkan GeneralResponse type pada file type.ts:

src\services\type.ts
export type GeneralResponse<T> = {
  message: string,
  data: T,
};

Buatlah file product.ts sebagai product services pada folder yang sama dan tambahkan code berikut:

src\services\product.ts
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import type { Product } from "@prisma/client";
import { APP_URL } from "@/constant";
import { GeneralResponse } from "./type";
import { addSearchParams } from "@/utils/url";
 
type ProductWithPagination = {
  products: Product[];
  totalCount: number;
  totalPages: number;
};
 
type ProductPaginationParams = {
  page: number;
  limit: number;
};
 
export const productApi = createApi({
  reducerPath: "productApi",
  baseQuery: fetchBaseQuery({ baseUrl: APP_URL }),
  endpoints: (builder) => ({
    getProducts: builder.query<
      GeneralResponse<ProductWithPagination>,
      ProductPaginationParams
    >({
      query: ({ page, limit }) => {
        //generate search params and append it to url
        const queryParams = addSearchParams([
          { name: "page", value: page.toString() },
          { name: "limit", value: limit.toString() },
        ]);
 
        return `/api/dashboard/products${queryParams}`;
      },
    }),
  }),
});
 
export const {
  useGetProductsQuery
} = productApi;

Code di atas bertanggung jawab sebagai service untuk memanggil API product yang sudah kita buat pada RKT Query, selain itu service ini juga men-generate hook secara otomatis lengkap dengan typing pada typescript.

Berikutnya jangan lupa tambahkan util baru yaitu url.ts untuk handle query search params pada FE:

src\utils\url.ts
export const addSearchParams = (
  queries: {
    name: string;
    value: string | undefined;
  }[]
) => {
  const newURL = new URL(window.location.href);
  const searchParams = new URLSearchParams(newURL.search);
 
  queries
    .filter((item) => Boolean(item.value))
    .forEach((query) => {
      searchParams.set(query.name, query.value as string);
    });
 
  newURL.search = searchParams.toString();
 
  // we only need searchParams
  return newURL.search.toString();
};

Selanjutnya, tambahkan product service pada root store redux kita:

src\store\index.ts
import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/dist/query";
 
import { productApi } from "@/services/product";
 
export const store = configureStore({
  reducer: {
    [productApi.reducerPath]: productApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(productApi.middleware),
});
 
setupListeners(store.dispatch);
 
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;

Selanjutnya buatlah folder constant tambahkan sebuah constant seperti berikut pada file index.ts:

src\constant\index.ts
export const APP_URL = process.env.NEXTAUTH_URL as string;
export const fallBackImage =
  "data:image/png;base64,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";

Step 3: Membuat page Products

Buatlah sebuah file page.tsx pada app, kemudian gunakan product service dan inject datanya ke component Table dari Ant Design:

src\app\dashboard\products\page.tsx
"use client";
 
import { Table } from "antd";
import React, { useMemo } from "react";
 
import { columns } from "./column";
 
import { useGetProductsQuery } from "@/services/product";
 
const DEFAULT_PAGE = 1;
const DEFAULT_LIMIT = 5;
 
const Products = () => {
  const page = DEFAULT_PAGE;
  const limit = DEFAULT_LIMIT;
 
  //load get products service
  const { data, isFetching, refetch } = useGetProductsQuery({
    page,
    limit,
  });
 
  const dataSource = useMemo(() => data?.data.products || [], [data]);
 
  return (
    <div>
      <Table
        columns={columns}
        rowKey={(record) => record.id}
        dataSource={dataSource}
      />
    </div>
  );
};
 
export default Products;

Selanjutnya buat file column.ts pada folder yang sama sebagai struktur dari kolom pada table:

src\app\dashboard\products\column.tsx
import { fallBackImage } from "@/constant";
import convertToRupiah from "@/utils/rupiah";
import { Product } from "@prisma/client";
import { Image } from "antd";
import { ColumnsType } from "antd/es/table";
 
export const columns: ColumnsType<Product> = [
  {
    title: "Product Name",
    render: (value) => value.name,
  },
  {
    title: "Description",
    render: (value) => value.description,
    width: "30%",
  },
  {
    title: "Category",
    render: (value) => value.category.name,
  },
  {
    title: "Price",
    render: (value) => convertToRupiah(value.price),
  },
  {
    title: "Image",
    width: "20%",
    render: (value) => (
      <Image
        width={120}
        height={80}
        src={value.image}
        style={{
          objectFit: "cover",
          borderRadius: 5,
        }}
        fallback={fallBackImage}
      />
    ),
  },
];

Setelah itu, tambahkan utils baru yaitu rupiah.ts untuk mengkoversi number ke format rupiah:

src\utils\rupiah.ts
const convertToRupiah = (num: number) => {
  return num
    ? new Intl.NumberFormat("id-ID", {
        style: "currency",
        currency: "IDR",
      }).format(num)
    : "-";
};
 
export default convertToRupiah;

Jika sudah selesai berikut tampilannya pada http://localhost:3000/dashboard/products

Kita bisa lihat, bahwa data kita sudah muncul tetapi hanya 5. Alasannya, karena limit yang kita set masih bersifat static dan belum dinamis, sehingga API get products hanya return sebanyak 5 data.

Product Table

Bagaimana caranya agar kita punya fungsi pagination yang sesuai? Kamu bisa merubah variable page dan limit menjadi state agar dinamis, tetapi pada tutorial kali ini author akan mengarahkanmu mengunakan url search params, agar kedepan jika url dicopy, user lain yang membuka akan mendapatkan UI yang sama dengan apa yg kita lihat.

Modifikasi file page.tsx kemudian tambahkan logic berikut:

src\app\dashboard\products\page.tsx
"use client";
 
import { Table } from "antd";
import React, { useEffect, useMemo } from "react";
import { TablePaginationConfig } from "antd/es/table";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
 
import { columns } from "./column";
 
import { useGetProductsQuery } from "@/services/product";
import { addSearchParams } from "@/utils/url";
 
const DEFAULT_PAGE = 1;
const DEFAULT_LIMIT = 5;
 
const Products = () => {
 
  const router = useRouter();
  const pathname = usePathname(); // next js hook to get url path
  const searchParams = useSearchParams(); // next js hook to get url search params
 
// store our url state in this variable, when it change trigger refetch to BE
  const page = parseInt(searchParams.get("page") as string) || DEFAULT_PAGE;
  const limit = parseInt(searchParams.get("limit") as string) || DEFAULT_LIMIT;
 
  const { data, isFetching, refetch } = useGetProductsQuery({
    page,
    limit,
  });
 
  const dataSource = useMemo(() => data?.data.products || [], [data]);
  const totalDataCount = useMemo(() => data?.data.totalCount || 0, [data]);
 
  useEffect(() => {
    // Fetch todos when the page, limit or filter changes
    refetch();
  }, [page, limit, refetch]);
 
 
// handle when table change, push to new route with new url search params
  const handleTableChange = (pagination: TablePaginationConfig) => {
    router.push(
      pathname +
        addSearchParams([
          { name: "page", value: pagination.current?.toString() },
          { name: "limit", value: pagination.pageSize?.toString() },
        ])
    );
  };
 
  return (
    <div>
      <Table
        columns={columns}
        rowKey={(record) => record.id}
        dataSource={dataSource}
        pagination={{
          current: page,
          pageSize: limit,
          total: totalDataCount,
          showSizeChanger: true,
          pageSizeOptions: ["5", "10", "20", "50"],
          showTotal: (total, [num1, num2]) => {
            return `Showing: ${num1} to ${num2} from ${total} rows`;
          },
        }}
        loading={isFetching}
        onChange={handleTableChange}
      />
    </div>
  );
};
 
export default Products;

Berikut penjelasan dari kode diatas:

Kita mendefinisikan constant DEFAULT_PAGE dan DEFAULT_LIMIT yang digunakan sebagai value default untuk page dan batas data yang ditampilkan per page.

Di dalam component Products, kita menggunakan beberapa hooks dari Next.js seperti useRouter, usePathname, dan useSearchParams untuk mendapatkan informasi terkait URL.

Selanjutnya, kita menggunakan hook useGetProductsQuery dari service product untuk mendapatkan data produk dari backend dengan menyertakan parameter page dan limit yang berasal dari URL.

Data produk yang diterima dari backend disimpan dalam variabel dataSource, sedangkan total jumlah data disimpan dalam variabel totalDataCount.

Pada useEffect, kita menggunakan function refetch untuk memanggil kembali product service ketika terjadi perubahan pada page dan limit, perubahan ini di trigger oleh sebuah function yaitu handleTableChange. Function ini akan dipanggil saat terjadi perubahan pada Table, seperti perubahan page atau perubahan jumlah data pada table, kemudian function ini akan merubah URL, dan ketika URL berubah useEffect terpanggil lalu function refetch ter-trigger kembali sehingga data yang muncul adalah data page yang dituju.

Berikut tampilan dari aplikasi kita, kamu dapat bernavigasi ke page yang dinginkan & merubah jumlah limt data per page.

Product Pagination Table

Perhatikan, ketika berpindah ke halaman 2 url pada aplikasi akan berubah menjadi:

http://localhost:3000/dashboard/products?page=2&limit=5

Menariknya jika kamu paste url ini pada browser tab baru, maka UI yang kita dapatkan akan sama persis dengan tampilan apliaksi kita sebelumnya.

Step 4: Tambahkan Fitur Pencarian Nama Product

Modifikasi API GET products dengan menambahkan paramater baru, yaitu "name" dan tambahkan fungsi filter by name:

src\app\api\dashboard\products\route.ts
import { NextResponse, NextRequest } from "next/server";
 
import prisma from "@/lib/prisma";
 
const { json: jsonResponse } = NextResponse;
 
export const GET = async (request: NextRequest) => {
  const url = new URL(request.url);
 
  // Current page (default: 1)
  const page = parseInt(url.searchParams.get("page") as string) || 1;
 
  // Number of items per page (default: 10)
  const limit = parseInt(url.searchParams.get("limit") as string) || 10;
 
  const name = url.searchParams.get("name") || "";
 
  try {
    //add filter by name if existed
    let where = {};
 
    if (name) {
      where = {
        name: {
          contains: name.trim().toLowerCase(),
          mode: "insensitive",
        },
      };
    }
 
    // Get total count of todos
    const totalCount = await prisma.product.count({
      where,
    });
 
    // Calculate total number of pages
    const totalPages = Math.ceil(totalCount / limit);
 
    const products = await prisma.product.findMany({
      skip: (page - 1) * limit, // Calculate the number of items to skip based on the current page and limit
      take: limit, // Set the number of items to retrieve per page
      include: {
        category: true,
      },
      where,
    });
 
    return jsonResponse(
      {
        message: "Successfully get products",
        data: {
          products,
          totalCount,
          totalPages,
        },
      },
      {
        status: 200,
      }
    );
  } catch (_error) {
    return jsonResponse(
      {
        message: "Server Error",
      },
      {
        status: 500, //server error status code
      }
    );
  }
};

Pada method getProducts pada product services tambahkan parameter filter

src\services\product.ts
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import type { Product } from "@prisma/client";
import { APP_URL } from "@/constant";
import { GeneralResponse } from "./type";
import { addSearchParams } from "@/utils/url";
 
type ProductWithPagination = {
  products: Product[];
  totalCount: number;
  totalPages: number;
};
 
type ProductPaginationParams = {
  page: number;
  limit: number;
  filter?: {
    name?: string;
  };
};
 
export const productApi = createApi({
  reducerPath: "productApi",
  baseQuery: fetchBaseQuery({ baseUrl: APP_URL }),
  endpoints: (builder) => ({
    getProducts: builder.query<
      GeneralResponse<ProductWithPagination>,
      ProductPaginationParams
    >({
      query: ({ page, limit, filter }) => {
        const queryParams = addSearchParams([
          { name: "page", value: page.toString() },
          { name: "limit", value: limit.toString() },
          { name: "name", value: filter?.name },
        ]);
 
        return `/api/dashboard/products${queryParams}`;
      },
    }),
  }),
});
 
export const {
  useGetProductsQuery
} = productApi;

Pada page products tambahkan url search params baru yaitu "name", juga component Input:

src\app\dashboard\products\page.tsx
"use client";
 
//..previous code
 
const Products = () => {
  //..previous code
 
  const page = parseInt(searchParams.get("page") as string) || DEFAULT_PAGE;
  const limit = parseInt(searchParams.get("limit") as string) || DEFAULT_LIMIT;
  const searchName = searchParams.get("name") || "";
 
  const { data, isFetching, refetch } = useGetProductsQuery({
    page,
    limit,
    filter: {
      name: searchName,
    },
  });
 
   const handleTableChange = (pagination: TablePaginationConfig) => {
    router.push(
      pathname +
        addSearchParams([
          { name: "page", value: pagination.current?.toString() },
          { name: "limit", value: pagination.pageSize?.toString() },
          { name: "name", value: searchName }
        ])
    );
  };
 
  //..previous code
 
  return (
    <div>
      <div className="mb-6 flex items-center w-full justify-end">
        <div>
          <Input.Search
            defaultValue={searchName}
            size="large"
            placeholder="Search Product..."
            onSearch={(value) =>
              router.push(
                pathname +
                  `?page=${DEFAULT_PAGE}&limit=${DEFAULT_LIMIT}&name=${value}`
              )
            }
          />
        </div>
      </div>
      <Table
        // ...other props
      />
    </div>
  );
};
 
export default Products;

Berikut tampilan dari aplikasi, kamu dapat melakukan pencarian dengan cara mengklik tombol pencarian atau tombol enter pada keyboard.

Product Search Table

Selamat kamu sudah selesai membuat datatable server side untuk products.

Pada tutorial berikutnya kita akan belajar bagaimana caranya menambahkan fungsi create, update dan delete product.

Code untuk tutorial kali ini link repo, jangan lupa star di repo ya!

Stay tune dan jangan lupa share & support Tahu Coding, Terima Kasih!