Seni Menerima Data dari Backend, untuk Frontend yang Ingin Tidur Nyenyak

Seni Menerima Data dari Backend, untuk Frontend yang Ingin Tidur Nyenyak

Zakiego

Zakiego

@zakiego

Abstrak

Data yang didapat saat melakukan fetch ke API selalu memiliki type any. Kita tidak pernah benar-benar tahu, apa bentuk dari data tersebut. Untuk itu, saat menerima data, kita harus memastikan bahwa data tersebut sesuai dengan apa yang kita inginkan.

Jika data tersebut berbeda dengan ekspektasi kita, ia harus memunculkan error, bukan malah berjalan normal seperti tidak ada apa-apa.

zod menjadi library yang digunakan untuk memvalidasi data dalam tulisan ini.

Coba secara langsung melalui zod-undefined-playground.vercel.app

Pendahuluan

Saat bekerja dalam tim yang kompleksitasnya menengah ke atas, setidaknya terdapat dua role, yaitu Frontend Engineer dan Backend Engineer. Sederhananya, Frontend adalah seorang yang membuat tampilan web agar terlihat cantik, sedangkan Backend adalah orang di balik layar yang mengelola keluar masuknya data ke database.

Jalur komunikasi paling lazim di antara dua role ini biasanya menggunakan API.

Seorang Backend akan menyediakan endpoint, contoh, dummyjson.com/products. Kemudian, Frontend akan melakukan fetch ke endpoint tersebut:

const resp = await fetch("https://dummyjson.com/products").then((res) =>
  res.json(),
);

console.log(resp);

Ketidakpastian

Kita hidup di dunia yang penuh ketidakpastian. Dan, satu-satu halnya yang pasti di dunia ini adalah ketidakpastian itu sendiri.

Begitu pula halnya yang dialami seorang Frontend.

Misalkan, saat melakukan fetch ke API di atas, kita mendapatkan response seperti berikut:

{
  "products": [
    {
      "id": 1,
      "title": "iPhone 9",
      "description": "An apple mobile which is nothing like apple",
      "cost": 549,
      "discountPercentage": 12.96,
      "rating": 4.69,
      "stock": 94,
      "brand": "Apple",
      "category": "smartphones",
      "thumbnail": "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg",
      "images": [
        "https://cdn.dummyjson.com/product-images/1/1.jpg",
        "https://cdn.dummyjson.com/product-images/1/2.jpg",
        "https://cdn.dummyjson.com/product-images/1/3.jpg",
        "https://cdn.dummyjson.com/product-images/1/4.jpg",
        "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg"
      ]
    },
    {
      "id": 2,
      "title": "iPhone X",
      "description": "SIM-Free, Model A19211 6.5-inch Super Retina HD display with OLED technology A12 Bionic chip with ...",
      "cost": 899,
      "discountPercentage": 17.94,
      "rating": 4.44,
      "stock": 34,
      "brand": "Apple",
      "category": "smartphones",
      "thumbnail": "https://cdn.dummyjson.com/product-images/2/thumbnail.jpg",
      "images": [
        "https://cdn.dummyjson.com/product-images/2/1.jpg",
        "https://cdn.dummyjson.com/product-images/2/2.jpg",
        "https://cdn.dummyjson.com/product-images/2/3.jpg",
        "https://cdn.dummyjson.com/product-images/2/thumbnail.jpg"
      ]
    }
  ]
}

Data yang didapat adalah data product berupa array, yang berisi object detail dari tiap product. Kemudian data tersebut kita render ke tiap component, untuk ditampilkan. Ada nama product, harga, rating, image, dll. Semua berjalan aman.

Coba melalui playground berikut ini zod-undefined-playground.vercel.app

Namun ternyata, pada suatu hari dan detik yang tidak kita ketahui, Backend mengubah format data yang diberikan.

Datanya berubah menjadi berikut:

{
  "products": [
    {
      "id": 1,
      "title": "iPhone 9",
      "description": "An apple mobile which is nothing like apple",
      "price": 549,
      "discountPercentage": 12.96,
      "rating": 4.69,
      "stock": 94,
      "brand": "Apple",
      "category": "smartphones",
      "thumbnail": "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg",
      "images": [
        "https://cdn.dummyjson.com/product-images/1/1.jpg",
        "https://cdn.dummyjson.com/product-images/1/2.jpg",
        "https://cdn.dummyjson.com/product-images/1/3.jpg",
        "https://cdn.dummyjson.com/product-images/1/4.jpg",
        "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg"
      ]
    },
    {
      "id": 2,
      "title": "iPhone X",
      "description": "SIM-Free, Model A19211 6.5-inch Super Retina HD display with OLED technology A12 Bionic chip with ...",
      "price": 899,
      "discountPercentage": 17.94,
      "rating": 4.44,
      "stock": 34,
      "brand": "Apple",
      "category": "smartphones",
      "thumbnail": "https://cdn.dummyjson.com/product-images/2/thumbnail.jpg",
      "images": [
        "https://cdn.dummyjson.com/product-images/2/1.jpg",
        "https://cdn.dummyjson.com/product-images/2/2.jpg",
        "https://cdn.dummyjson.com/product-images/2/3.jpg",
        "https://cdn.dummyjson.com/product-images/2/thumbnail.jpg"
      ]
    }
  ]
}

Apa yang berubah? Jika dilihat secara sekilas tidak ada yang berubah. Semua terlihat sama. Namun saat dilihat lebih detail, barulah kita sadar, bahwa terjadi perubahan pada salah satu key, dari cost menjadi price.

Mari lihat kode di bawah ini. Apa yang terjadi saat kita mencoba melakukan print cost (seharusnya price).

const resp = await fetch("https://dummyjson.com/products").then((res) =>
  res.json(),
);

// seharusnya adalah `price`, bukan `cost`
console.log(resp.products[0].cost); 
// undefined

Hasilnya hanya undefined, bukan error.

Dan ketika ditampilkan ke halaman web semua berjalan lancar, tidak ada error. Namun harga dari barang menjadi kosong.

Coba melalui playground berikut ini zod-undefined-playground.vercel.app/without-zod

Lalu apa masalahnya? Masalahnya adalah:

  1. Jika perubahan yang dilakukan oleh Backend ini terjadi tanpa kita sadari. Kita tidak mendapat kabar bahwa terjadi perubahan format pada response API. Alhasil, logic dari aplikasi yang kita buat akan berjalan di luar perkiraan kita.
  2. Karena return hanya berupa undefined, bukan error, maka kita tidak akan mendapatkan pemberitahuan error. Artinya aplikasi tetap berjalan seperti biasa, namun sebenarnya ada sesuatu yang salah terjadi. Misalnya, harga yang seharusnya tampil, menjadi kosong, karena kesalahan key. Tidak ada error, namun hanya kosong. Kita tidak akan tahu hal tersebut, sampai kita menemukannya sendiri, atau ada laporan yang masuk.

Pengaman

Izinkan saya memperkenalkan zod (zod.dev).

Dengan API yang sama seperti sebelumnya, data response yang diterima, kemudian divalidasi terlebih dahulu, dengan skema data yang kita inginkan. Setelah itu, barulah data digunakan.

import { z } from "zod";

// response didapat dari fetch selalu memiliki type any`
const resp = await fetch("https://dummyjson.com/products").then((res) =>
  res.json(),
);

// definisikan skema data yang diinginkan
const schema = z.object({
  products: z.array(
    z.object({
      id: z.number(),
      title: z.string(),
      description: z.string(),
      cost: z.number(), // <--- masih menggunakan `cost`, bukan `price`
      discountPercentage: z.number(),
      rating: z.number(),
      stock: z.number(),
      brand: z.string(),
      category: z.string(),
      thumbnail: z.string(),
      images: z.array(z.string()),
    }),
  ),
});

// data di-parse menggunakan skema yang sudah didefinisikan
const data = schema.parse(resp);

console.log(data);

Skema zod di atas berasumsi key dari harga masih menggunakan cost, bukan price.

Lihat hasilnya saat dijalankan.

ZodError: [
  {
    "code": "invalid_type",
    "expected": "number",
    "received": "undefined",
    "path": [
      "products",
      0,
      "cost"
    ],
    "message": "Required"
  }
]

Dan...… error. Itulah yang seharusnya terjadi.

Coba melalui playground berikut ini zod-undefined-playground.vercel.app/with-zod

Error terjadi karena skema yang kita definisikan, harus ada key berupa cost dengan value dengan tipe number. Namun yang didapat, value dari cost justru memiliki tipe undefined.

Saat data yang didapat tidak sesuai ekspektasi kita, maka dia harus menampilkan error. Tujuannya, agar kita bisa membenahi hal tersebut secepat mungkin.

Apa itu Zod?

Rasanya-rasanya kita terlalu jauh melangkah, mari ambil beberapa langkah ke belakang, untuk mengetahui, apa itu sebenarnya zod?

Zod is a TypeScript-first schema declaration and validation library.

Dari definisi di atas, zod adalah library untuk mendefinisikan skema dan memvalidasi data. Validasi data diperlukan untuk menjaga dan memastikan konsistensi data, agar programmer bisa tidur nyenyak, tanpa takut data yang digunakan tiba-tiba berubah tanpa sepengetahuan.

Mari kita coba, pelan-pelan.

Pertama, install zod ke project kita.

pnpm add zod

Kemudian, import dan buat skema sederhana.

Kita membuat sebuah skema bernama rankSchema yang mengharuskan untuk menerima input berupa number. Namun, alih-alih memasukkan number, kita mencoba memasukkan string.

Lihat bagaimana hasilnya.

import { z } from "zod";

const rankSchema = z.number(); // skema hanya menerima input berupa number

const rank = rankSchema.parse("2"); // kita mencoba memasukkan string

console.log(rank);

// ZodError: [
//   {
//     code: "invalid_type",
//     expected: "number",
//     received: "string",
//     path: [],
//     message: "Expected number, received string",
//   },
// ];

Hasilnya adalah error. Karena kita memaksa, agar input yang dibolehkan hanya benar-benar number, namun input yang diterima justru string.

Jika input yang dimasukkan benar-benar number, maka hasilnya tidak akan ada error.

import { z } from "zod";

const rankSchema = z.number();

const rank = rankSchema.parse(1);

console.log(rank);
// 1

Mari lanjut untuk membuat skema yang lebih kompleks.

Kita mendefinisikan sebuah skema untuk user bernama userSchema, data yang di-input harus berupa object, dengan minimal terdapat key name dan age. Kemudian kita masukkan input data sesuai skema tersebut.

import { z } from "zod";

const userSchema = z.object({
  name: z.string(),
  age: z.number(),
});

const input = {
  name: "Zaki",
  age: 20,
};

const user = userSchema.parse(input);

console.log(user);
// {
//  name: "Zaki",
//  age: 20,
// }

Hasilnya, semua berjalan aman, karena skema dan input sesuai. Bagus.

Kemudian kita coba, seandainya input yang diberikan tidak sesuai. Key age dihapus dari object.

import { z } from "zod";

const userSchema = z.object({
  name: z.string(),
  age: z.number(),
});

const input = {
  name: "Zaki", // <--– age dihapus
};

const user = userSchema.parse(input);

console.log(user);

// ZodError: [
//   {
//     code: "invalid_type",
//     expected: "number",
//     received: "undefined",
//     path: ["age"],
//     message: "Required",
//   },
// ];

Karena age kita hapus, sedangkan di schema age bersifat wajib, maka muncullah error.

Pilihan Skema

Banyak pilihan type yang bisa digunakan untuk mendefinisikan skema. Berikut beberapa contohnya:

// primitive values
z.string();
z.number();
z.bigint();
z.boolean();
z.date();
z.symbol();

// empty types
z.undefined();
z.null();
z.void(); // accepts undefined

// catch-all types
// allows any value
z.any();
z.unknown();

// never type
// allows no values
z.never();

z.enum(["Salmon", "Tuna", "Trout"]);

Lengkapnya bisa dilihat di zod.dev.

Tidak semua value bersifat wajib, kita bisa menjadikannya opsional, misal z.string().optional().

Atau jika kita tidak tahu (atau malas mendefinisikan) kita bisa menggunakan z.any() 🤷🏻‍♂️.

Type Inference

Keuntungan lain dari menggunakan schema validation adalah kita bisa sekaligus mendapatkan Type, dari skema yang sudah didefinisikan.

import { z } from "zod";

const userSchema = z.object({
  name: z.string(),
  age: z.number(),
});

type User = z.infer<typeof userSchema>;
//   ^ type User = { name: string; age: number; }

Sehingga, jika kita memerlukan type dari User, sebagai type dari props di sebuah component, kita hanya perlu mengekspornya.

Penutup

Data yang didapat dari fetch selalu bernilai any. Kita tidak pernah benar-benar tau, apa bentuk dari data tersebut. Seperti membeli kucing di dalam karung, isinya bisa saja sebenarnya adalah buaya.

Untuk itu, perlu untuk melakukan validasi dari data yang kita dapat.

Agar tidur kita menjadi lebih tenang.

Buah Tangan

Berikut beberapa resource yang relevan dengan tulisan ini:

Alternatif

zod bukan satu-satunya schema declaration and validation library.

Ada alternatif lain seperti valibot dan arktype.

Saya pribadi masih menjadi pengguna zod, tapi silakan pilih sesuai keperluan.


Selesai ditulis pada Jumat, 12 April 2024 pukul 23:41 di Pelaihari, Kalimantan Selatan

Selesai disempurnakan pada Sabtu, 13 April 2024 pukul 18:59 di Pelaihari, Kalimantan Selatan