proyekapigenerictugasMahir6 mnt baca

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

typescript
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:

typescript
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

typescript
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

typescript
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:

typescript
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 T dengan schema.parse(data) dari Zod, tambah timeout via AbortSignal.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).

typescript
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:

typescript
interface Client {
  get<T>(path: string): Promise<T>;
  post<T>(path: string, body: unknown): Promise<T>;
}

Syarat:

  • get dan post generic, tanpa any di mana pun
  • Error dilempar sebagai object { status: number; pesan: string }
  • post mengirim JSON dengan header Content-Type yang benar
  • Sertakan contoh pemakaian dengan interface Produk untuk 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?