Membangun Klien API Type-Safe di TypeScript: Lebih dari Sekadar Axios vs Fetch
Pendahuluan: Mengapa Type Safety Penting untuk Klien API Anda?
Dalam dunia pengembangan web modern, berinteraksi dengan API adalah inti dari hampir setiap aplikasi. Baik Anda mengambil data dari backend, mengirim informasi, atau mengautentikasi pengguna, klien API Anda adalah jembatan antara aplikasi Anda dan dunia luar. Menggunakan TypeScript untuk membangun klien API Anda menawarkan keuntungan besar, terutama dalam hal type safety.
Type safety berarti bahwa kompiler TypeScript dapat memverifikasi jenis data yang Anda gunakan dalam kode Anda. Hal ini membantu mencegah kesalahan pada saat runtime yang disebabkan oleh jenis data yang tidak terduga atau tidak cocok. Bayangkan skenario berikut:
- Anda mengharapkan API mengembalikan objek dengan properti `nama` yang berupa string, tetapi terkadang API mengembalikan `null`. Tanpa type safety, kode Anda mungkin macet saat Anda mencoba mengakses `nama.length`.
- Anda mengirim data ke API, tetapi API mengharapkan properti tertentu dengan jenis tertentu. Tanpa type safety, Anda mungkin mengirim data yang salah dan menerima respons kesalahan yang membingungkan.
Type safety membantu Anda menangkap kesalahan ini selama pengembangan, sebelum mereka memengaruhi pengguna Anda. Ini juga membuat kode Anda lebih mudah dibaca dan dipahami, karena jenis data yang Anda gunakan didefinisikan secara eksplisit.
Artikel ini akan membawa Anda melampaui perbandingan sederhana antara Axios dan Fetch dan menyelami bagaimana Anda dapat membangun klien API yang benar-benar type-safe di TypeScript. Kita akan membahas:
- Mengapa type safety penting dalam pengembangan klien API.
- Kelemahan Axios dan Fetch dalam kaitannya dengan type safety.
- Teknik-teknik lanjutan untuk membangun klien API type-safe, termasuk penggunaan:
- Generics
- Tipe data yang didefinisikan (Interfaces dan Types)
- Validasi runtime dengan pustaka seperti Zod atau io-ts
- Pembuatan kode (code generation) dari OpenAPI/Swagger
- Contoh praktis dari klien API type-safe dengan berbagai teknik.
- Praktik terbaik untuk memelihara dan mengelola klien API type-safe Anda.
Mengapa Axios dan Fetch Saja Tidak Cukup untuk Type Safety?
Axios dan Fetch adalah dua pustaka populer untuk membuat permintaan HTTP di JavaScript dan TypeScript. Keduanya menawarkan cara mudah untuk berinteraksi dengan API, tetapi mereka memiliki keterbatasan dalam hal type safety. Mari kita telaah lebih dalam:
Axios
Axios adalah pustaka berbasis promise untuk membuat permintaan HTTP. Ini memiliki API yang sederhana dan mudah digunakan, dan mendukung fitur-fitur seperti interseptor, pembatalan permintaan, dan perlindungan XSRF. Namun, secara default, Axios tidak menyediakan type safety yang kuat.
Meskipun Anda dapat menggunakan generik dengan Axios untuk menentukan jenis data respons, Anda masih perlu melakukan type assertion secara manual. Contoh:
interface User {
id: number;
name: string;
email: string;
}
async function getUser(id: number): Promise<User> {
const response = await axios.get<User>(`/users/${id}`);
return response.data; // Anda masih perlu yakin bahwa `response.data` sesuai dengan tipe `User`
}
Masalah dengan pendekatan ini adalah bahwa kompiler tidak akan memberi Anda peringatan jika API mengembalikan data yang tidak sesuai dengan tipe `User`. Anda mungkin baru menemukan kesalahan pada saat runtime.
Fetch
Fetch adalah API standar web untuk membuat permintaan HTTP. Ini dibangun ke dalam browser modern dan tersedia di Node.js melalui pustaka seperti `node-fetch`. Seperti Axios, Fetch tidak menyediakan type safety yang kuat secara default.
Dengan Fetch, Anda perlu melakukan parsing respons secara manual dan kemudian melakukan type assertion. Contoh:
interface User {
id: number;
name: string;
email: string;
}
async function getUser(id: number): Promise<User> {
const response = await fetch(`/users/${id}`);
const data = await response.json(); // `data` bertipe `any`
return data as User; // Anda harus melakukan type assertion secara manual
}
Seperti dengan Axios, melakukan type assertion secara manual rentan terhadap kesalahan. Jika API mengembalikan data yang tidak sesuai dengan tipe `User`, kompiler tidak akan memberi Anda peringatan, dan Anda mungkin baru menemukan kesalahan pada saat runtime.
Ringkasan Keterbatasan
Baik Axios maupun Fetch memiliki keterbatasan dalam hal type safety karena mereka tidak memaksa tipe data respons pada saat kompilasi. Mereka mengandalkan pengembang untuk melakukan type assertion secara manual, yang rentan terhadap kesalahan. Dalam bagian selanjutnya, kita akan membahas teknik-teknik untuk membangun klien API yang benar-benar type-safe di TypeScript.
Teknik Lanjutan untuk Membangun Klien API Type-Safe
Untuk mengatasi keterbatasan Axios dan Fetch, kita dapat menggunakan teknik-teknik lanjutan untuk membangun klien API yang benar-benar type-safe di TypeScript. Berikut adalah beberapa teknik yang paling efektif:
1. Generics
Generics memungkinkan Anda untuk menulis kode yang dapat bekerja dengan berbagai jenis data tanpa harus menentukan jenis tersebut secara eksplisit. Ini sangat berguna untuk klien API, di mana Anda sering perlu bekerja dengan berbagai jenis data respons.
Anda dapat menggunakan generik untuk menentukan tipe data respons saat Anda membuat permintaan API. Contoh:
async function get<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json() as T;
}
interface User {
id: number;
name: string;
email: string;
}
async function getUser(id: number): Promise<User> {
return await get<User>(`/users/${id}`);
}
Dalam contoh ini, fungsi `get` menggunakan generik `T` untuk menentukan tipe data respons. Ketika Anda memanggil fungsi `get`, Anda menentukan tipe data respons yang Anda harapkan. TypeScript kemudian akan menggunakan informasi ini untuk memverifikasi bahwa respons API sesuai dengan tipe yang Anda harapkan.
2. Tipe Data yang Didefinisikan (Interfaces dan Types)
Interfaces dan Types memungkinkan Anda untuk menentukan struktur data yang Anda gunakan dalam kode Anda. Ini sangat berguna untuk klien API, di mana Anda perlu bekerja dengan data yang memiliki struktur yang kompleks.
Anda dapat menggunakan interfaces dan types untuk menentukan struktur data respons API. Contoh:
interface User {
id: number;
name: string;
email: string;
}
type Post = {
id: number;
title: string;
body: string;
userId: number;
};
async function getUser(id: number): Promise<User> {
const response = await fetch(`/users/${id}`);
const user: User = await response.json();
return user;
}
async function getPost(id: number): Promise<Post> {
const response = await fetch(`/posts/${id}`);
const post: Post = await response.json();
return post;
}
Dalam contoh ini, kita mendefinisikan interfaces `User` dan type `Post` untuk menentukan struktur data respons API. Ketika kita menerima respons API, kita melakukan type assertion untuk memastikan bahwa data sesuai dengan struktur yang kita definisikan. Jika data tidak sesuai, TypeScript akan memberi kita peringatan.
3. Validasi Runtime dengan Pustaka seperti Zod atau io-ts
Meskipun TypeScript menyediakan type safety pada saat kompilasi, itu tidak menjamin bahwa data yang Anda terima pada saat runtime sesuai dengan tipe yang Anda harapkan. API dapat berubah, atau data mungkin rusak selama transmisi.
Untuk mengatasi masalah ini, Anda dapat menggunakan pustaka validasi runtime seperti Zod atau io-ts. Pustaka ini memungkinkan Anda untuk memvalidasi data pada saat runtime untuk memastikan bahwa data sesuai dengan tipe yang Anda harapkan.
Zod
Zod adalah pustaka validasi skema TypeScript-first dengan inferensi tipe yang hebat. Ini memungkinkan Anda untuk mendefinisikan skema data Anda dan kemudian memvalidasi data terhadap skema tersebut pada saat runtime.
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
async function getUser(id: number): Promise<User> {
const response = await fetch(`/users/${id}`);
const data = await response.json();
try {
return UserSchema.parse(data);
} catch (error) {
console.error("Validation error:", error);
throw new Error("Invalid user data received from API");
}
}
Dalam contoh ini, kita menggunakan Zod untuk mendefinisikan skema data `UserSchema`. Kita kemudian menggunakan `UserSchema.parse(data)` untuk memvalidasi data terhadap skema tersebut pada saat runtime. Jika data tidak sesuai dengan skema, `parse` akan melempar kesalahan, yang kita tangkap dan log. Ini membantu kita untuk menangkap kesalahan validasi data pada saat runtime dan mencegah kesalahan merambat ke kode kita.
io-ts
io-ts adalah pustaka lain untuk validasi runtime dan serialisasi/deserialisasi data. Ini didasarkan pada konsep *codec*, yang merupakan objek yang dapat mengenkode dan mendekode data.
import * as t from 'io-ts'
import { PathReporter } from 'io-ts/PathReporter'
const User = t.type({
id: t.number,
name: t.string,
email: t.string
})
type User = t.TypeOf<typeof User>
async function getUser(id: number): Promise<User> {
const response = await fetch(`/users/${id}`);
const data = await response.json();
const validation = User.decode(data)
if (validation._tag === 'Left') {
console.error("Validation error:", PathReporter.report(validation));
throw new Error("Invalid user data received from API");
}
return validation.right
}
Mirip dengan Zod, io-ts memungkinkan kita mendefinisikan tipe data kita sebagai codec dan kemudian menggunakan codec tersebut untuk memvalidasi data pada saat runtime. Keuntungan menggunakan io-ts adalah fokusnya pada kompositionalitas dan kemampuan untuk mendefinisikan tipe data yang kompleks dengan cara yang modular.
4. Pembuatan Kode (Code Generation) dari OpenAPI/Swagger
Jika Anda menggunakan API yang memiliki spesifikasi OpenAPI/Swagger, Anda dapat menggunakan generator kode untuk menghasilkan klien API type-safe secara otomatis. Ini adalah cara yang bagus untuk memastikan bahwa klien API Anda selalu sinkron dengan API yang mendasarinya.
Ada banyak generator kode OpenAPI/Swagger yang tersedia, baik open-source maupun komersial. Beberapa yang populer meliputi:
Generator kode ini akan membaca spesifikasi OpenAPI/Swagger API Anda dan menghasilkan kode TypeScript yang mencakup:
- Interfaces untuk tipe data API
- Fungsi untuk membuat permintaan API
- Validasi data respons
Dengan menggunakan generator kode, Anda dapat menghindari kesalahan manusia dalam menulis klien API dan memastikan bahwa klien API Anda selalu sesuai dengan spesifikasi API.
Contoh menggunakan `openapi-typescript-codegen`:
- Instal `openapi-typescript-codegen`: `npm install -g openapi-typescript-codegen`
- Jalankan generator kode: `openapi –input [path/to/your/openapi.yaml] –output [path/to/your/output/directory]`
- Impor kode yang dihasilkan ke proyek Anda dan gunakan fungsi-fungsi yang dihasilkan untuk membuat permintaan API.
Contoh Praktis Klien API Type-Safe
Mari kita gabungkan teknik-teknik yang telah kita pelajari dan buat contoh praktis klien API type-safe untuk API JSONPlaceholder (https://jsonplaceholder.typicode.com/).
Pertama, kita akan mendefinisikan interfaces untuk data yang akan kita gunakan:
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
interface User {
id: number;
name: string;
username: string;
email: string;
address: {
street: string;
suite: string;
city: string;
zipcode: string;
geo: {
lat: string;
lng: string;
};
};
phone: string;
website: string;
company: {
name: string;
catchPhrase: string;
bs: string;
};
}
Selanjutnya, kita akan membuat fungsi untuk membuat permintaan API menggunakan Fetch dan generics, dan kita akan menggunakan Zod untuk validasi runtime:
import { z } from "zod";
const PostSchema = z.object({
userId: z.number(),
id: z.number(),
title: z.string(),
body: z.string(),
});
const UserSchema = z.object({
id: z.number(),
name: z.string(),
username: z.string(),
email: z.string().email(),
address: z.object({
street: z.string(),
suite: z.string(),
city: z.string(),
zipcode: z.string(),
geo: z.object({
lat: z.string(),
lng: z.string(),
}),
}),
phone: z.string(),
website: z.string(),
company: z.object({
name: z.string(),
catchPhrase: z.string(),
bs: z.string(),
}),
});
async function get<T>(url: string, schema: z.ZodSchema<T>): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
try {
return schema.parse(data);
} catch (error) {
console.error("Validation error:", error);
throw new Error(`Invalid data received from API: ${error}`);
}
}
async function getPost(id: number): Promise<Post> {
return await get<Post>(`https://jsonplaceholder.typicode.com/posts/${id}`, PostSchema);
}
async function getUser(id: number): Promise<User> {
return await get<User>(`https://jsonplaceholder.typicode.com/users/${id}`, UserSchema);
}
Akhirnya, kita dapat menggunakan fungsi-fungsi ini untuk mengambil data dari API:
async function main() {
try {
const post = await getPost(1);
console.log("Post:", post);
const user = await getUser(1);
console.log("User:", user);
} catch (error) {
console.error("Error:", error);
}
}
main();
Dalam contoh ini, kita telah membangun klien API type-safe yang menggunakan generics, interfaces, dan validasi runtime dengan Zod. Ini membantu kita untuk menangkap kesalahan pada saat kompilasi dan runtime, dan memastikan bahwa data yang kita gunakan sesuai dengan struktur yang kita harapkan.
Praktik Terbaik untuk Memelihara dan Mengelola Klien API Type-Safe
Setelah Anda membangun klien API type-safe Anda, penting untuk memelihara dan mengelolanya dengan baik. Berikut adalah beberapa praktik terbaik yang perlu dipertimbangkan:
- Jaga agar spesifikasi API Anda tetap up-to-date. Jika Anda menggunakan generator kode dari spesifikasi OpenAPI/Swagger, pastikan untuk memperbarui spesifikasi Anda setiap kali API berubah.
- Gunakan validasi runtime. Bahkan jika Anda menggunakan TypeScript, validasi runtime tetap penting untuk menangkap kesalahan yang mungkin terjadi pada saat runtime.
- Tulis tes unit. Tes unit dapat membantu Anda untuk memastikan bahwa klien API Anda berfungsi dengan benar dan bahwa itu dapat menangani berbagai skenario.
- Gunakan logging dan pemantauan. Logging dan pemantauan dapat membantu Anda untuk mengidentifikasi dan memecahkan masalah dengan klien API Anda.
- Refactor kode Anda secara teratur. Klien API Anda akan berkembang seiring waktu, jadi penting untuk melakukan refactoring kode Anda secara teratur untuk memastikan bahwa tetap mudah dibaca, dipahami, dan dipelihara.
Kesimpulan
Membangun klien API type-safe di TypeScript adalah investasi yang berharga. Ini membantu Anda untuk menangkap kesalahan pada saat pengembangan, membuat kode Anda lebih mudah dibaca dan dipahami, dan mengurangi risiko kesalahan pada saat runtime. Dengan menggunakan teknik-teknik yang telah kita bahas dalam artikel ini, Anda dapat membangun klien API yang benar-benar type-safe yang akan meningkatkan kualitas dan keandalan aplikasi Anda. Jangan hanya berfokus pada Axios vs Fetch, tetapi pertimbangkan teknik-teknik yang lebih canggih untuk memaksimalkan manfaat type safety di klien API Anda.
“`