Proyek: Type-Safe API Client
Bangun client API generik yang type-safe: request, response, dan error handling. Tugas dinilai AI.
Misi: Client API Tanpa any
Hampir setiap project frontend butuh memanggil REST API. Versi malasnya: fetch lalu as Data di mana-mana, error diabaikan, dan setiap endpoint mengulang boilerplate yang sama. Misi modul ini: bangun satu client generik kecil yang dipakai dengan type safety penuh dari request sampai response, tanpa satu pun any.
Kenapa penting? API berubah: backend menambah field atau mengubah format error. Dengan client generik yang rapi, perubahan response hanya menyentuh satu interface, dan compiler menunjuk semua pemakaian yang terdampak.
Langkah 1: Definisikan Bentuk Error
class ApiError extends Error {
constructor(
public status: number,
pesan: string
) {
super(pesan);
this.name = "ApiError";
}
}Memakai class memungkinkan instanceof di catch, sehingga pemanggil bisa membedakan error HTTP (ada status) dari error jaringan (fetch gagal total).
Langkah 2: Inti Generic request<T>
Satu fungsi menangani semua method HTTP. Generic <T> membiarkan pemanggil menentukan tipe response:
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(path, init);
if (!res.ok) {
throw new ApiError(res.status, `request gagal: ${res.status}`);
}
const data: unknown = await res.json();
return data as T;
}data dibaca sebagai unknown dulu, lalu as T di satu titik terpusat. Hanya satu tempat yang melakukan assertion, bukan di tiap pemanggilan. Di production, titik inilah yang diganti validasi Zod.
Langkah 3: Bungkus Jadi Client dengan get/post/put/hapus
interface Client {
get<T>(path: string): Promise<T>;
post<T>(path: string, body: unknown): Promise<T>;
put<T>(path: string, body: unknown): Promise<T>;
hapus(path: string): Promise<void>;
}
const BASE = "https://api.example.com";
export const api: Client = {
get: <T>(path: string): Promise<T> => request<T>(BASE + path),
post: <T>(path: string, body: unknown): Promise<T> =>
request<T>(BASE + path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}),
put: <T>(path: string, body: unknown): Promise<T> =>
request<T>(BASE + path, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}),
hapus: async (path) => {
await request<unknown>(BASE + path, { method: "DELETE" });
},
};Body bertipe unknown karena JSON.stringify menerima apa pun, dan header Content-Type wajib agar server mem-parse body sebagai JSON. Generic <T> diteruskan dari method client ke request.
Langkah 4: Pakai dengan Tipe Nyata + Error Handling
interface Produk {
id: string;
nama: string;
harga: number;
}
async function daftarProduk(): Promise<void> {
try {
const produk = await api.get<Produk[]>("/produk");
// produk: Produk[], autocomplete penuh di sini
produk.forEach((p) => console.log(p.nama, p.harga));
const baru = await api.post<Produk>("/produk", {
nama: "Kopi",
harga: 25000,
});
console.log("tersimpan:", baru.id);
} catch (e) {
if (e instanceof ApiError) {
console.error(`API error ${e.status}: ${e.message}`);
} else {
console.error("jaringan bermasalah", e);
}
}
}Pemanggil tidak pernah menulis any: tipe response mengalir dari generic <Produk[]>, dan error dipisah dengan instanceof.
Contoh Masalah yang Dicegah Client Ini
Tanpa client generik, pola umumnya seperti ini:
const res = await fetch("https://api.example.com/produk");
const produk = await res.json(); // produk: any
console.log(produk[0].nama.toUpperCase());
// Runtime error bila API mengembalikan { error: "..." }!
// TypeScript diam saja karena produk bertipe any.Tanpa cek status dan tanpa tipe, crash baru ketahuan di browser. Dengan client di atas, produk bertipe Produk[] sejak awal dan response gagal otomatis throw ApiError.
Catatan teknis: Di project nyata, ganti
as Tdenganschema.parse(data)dari Zod, tambah timeout viaAbortSignal.timeout(10000), dan sisipkan Authorization header otomatis. Fondasinya tetap: generic<T>, error class yang bisa di-instanceof, dan satu titik parsing terpusat.
Tantangan
Mulai dari get
Implementasikan dulu buatClient dengan method get<T> saja: fetch ke baseUrl + path, throw Error berisi status bila tidak ok, return await res.json() sebagai T. Test dengan get<{ nama: string }[]>("/users") (pakai URL dummy, yang penting lolos tsc).
interface Client {
get<T>(path: string): Promise<T>;
}
function buatClient(baseUrl: string): Client {
return {
async get<T>(path: string): Promise<T> {
// fetch, cek ok, return json sebagai T
},
};
}Tugas
Type-Safe API Client
Bangun buatClient(baseUrl: string): Client lengkap:
interface Client {
get<T>(path: string): Promise<T>;
post<T>(path: string, body: unknown): Promise<T>;
}Syarat:
getdanpostgeneric, tanpaanydi mana pun- Error dilempar sebagai object
{ status: number; pesan: string } postmengirim JSON dengan header Content-Type yang benar- Sertakan contoh pemakaian dengan interface
Produkuntuk get list dan post satu item
Kriteria penilaian:
- Generic dipakai dengan benar di get dan post
- Tidak ada any; data mentah ditangani via unknown
- Error handling dengan status dan pesan yang jelas
- Contoh pemakaian menunjukkan type safety (hover tipe benar)
AI tutor akan memeriksa bug, kesalahan syntax, dan memberi saran perbaikan.
Kuis Bab
Uji pemahamanmu: Praktik & Ekosistem
Jawab 5 soal berikut, lalu tekan "Periksa Jawaban".
1.Apa yang tidak bisa dilakukan dengan abstract class?
2.Apa status decorator di TypeScript saat ini?
3.Apa yang diaktifkan oleh strict: true?
4.Kenapa useState<User | null>(null) butuh generic eksplisit?
5.Apa langkah pertama yang benar saat menghadapi type error panjang dan membingungkan?