Seni Menerima Data dari Backend, untuk Frontend yang Ingin Tidur Nyenyak
Zakiego
@zakiego
Daftar Isi
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:
- 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.
- 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:
- When should you use Zod? - Matt Pocock
- github.com/total-typescript/zod-tutorial
- Belajar TypeScript Validation - Programmer Zaman Now
- Zod Tutorial - All 10 places for Zod in your React / Next.js app
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