14. API Integration: Advanced TypeScript Instance
Kita mengadopsi pola API Instance yang robust, namun ditingkatkan dengan TypeScript Strict Mode. Semua HTTP client wajib mengaktifkan withCredentials: true atau credentials: "include" agar cookie sesi (Better Auth) mengalir otomatis ke backend.
Standar Format Response & Error API
Untuk menjamin konsistensi integrasi di sisi client, backend dan frontend menyepakati struktur payload JSON standar untuk response sukses dan error:
1. Payload Sukses (2xx Success Response)
Setiap response sukses dibungkus dalam properti data, sehingga helper wrapper (seperti APIInstance.get) dapat langsung me-return bagian data tersebut untuk memangkas boilerplate .data.data di sisi React.
{
"success": true,
"message": "Data berhasil diambil",
"data": {
"id": "usr_9128",
"name": "Jane Doe",
"email": "jane@example.com",
"role": "ADMIN"
}
}
2. Payload Error (4xx / 5xx Error Response)
Response error yang dikirimkan server menyertakan properti code (untuk lokalisasi / penanganan error terprogram) serta objek errors untuk validasi field-level (misalnya form validation).
{
"success": false,
"message": "Validasi input gagal",
"code": "VALIDATION_ERROR",
"errors": {
"email": [
"Format email tidak valid"
],
"password": [
"Password minimal harus 8 karakter"
]
}
}
Struktur payload error di atas nantinya akan dinormalisasi oleh HTTP Client ke dalam tipe data TypeScript ApiError:
export interface ApiError {
message: string;
status: number;
code: string;
errors: Record<string, string[]> | null;
}
Contoh services/api.ts — Axios Instance + APIInstance Wrapper:
import axios, {
AxiosInstance,
AxiosResponse,
InternalAxiosRequestConfig,
AxiosRequestConfig,
} from "axios";
export interface ApiError {
message: string;
status: number;
code: string;
errors: Record<string, string[]> | null;
}
interface CustomAxiosConfig extends InternalAxiosRequestConfig {
skipAuth?: boolean;
}
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "https://api.my-app.com/v1";
const api: AxiosInstance = axios.create({
baseURL: BASE_URL,
timeout: 15000,
withCredentials: true, // Cookie sesi Better Auth mengalir otomatis ✅
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
// Request Interceptor
api.interceptors.request.use(
(config: CustomAxiosConfig) => {
config.headers["X-Requested-With"] = "XMLHttpRequest";
if (config.skipAuth) config.withCredentials = false;
return config;
},
(error) => Promise.reject(error),
);
// Queue untuk menangani request beruntun saat refresh token sedang berjalan
let isRefreshing = false;
let failedQueue: any[] = [];
const processQueue = (error: any, token: string | null = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
// Response Interceptor
api.interceptors.response.use(
(response: AxiosResponse) => response,
async (error) => {
const originalRequest = error.config;
// Jika response unauthorized (401) dan request belum ditandai retry
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// Jika proses refresh token sedang berjalan, masukkan request ke antrean
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then(() => {
return api(originalRequest);
})
.catch((err) => {
return Promise.reject(err);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
// Panggil endpoint refresh token (cookie HttpOnly terkirim otomatis jika withCredentials aktif)
await axios.post(
`${BASE_URL}/auth/refresh`,
{},
{ withCredentials: true }
);
isRefreshing = false;
processQueue(null);
// Ulangi request asli yang sempat tertunda
return api(originalRequest);
} catch (refreshError: any) {
isRefreshing = false;
processQueue(refreshError);
// Jika refresh token juga expired/gagal, arahkan ke login
if (typeof window !== "undefined") {
const path = window.location.pathname + window.location.search;
window.location.href = `/login?message=session_expired&callbackUrl=${encodeURIComponent(path)}`;
}
return Promise.reject(refreshError);
}
}
const normalizedError: ApiError = {
message: error.response?.data?.message || "Terjadi kesalahan pada server",
status: error.response?.status || 500,
code: error.response?.data?.code || "SERVER_ERROR",
errors: error.response?.data?.errors || null,
};
return Promise.reject(normalizedError);
},
);
// Wrapper terstandardisasi — konsumsi langsung tanpa .data
export const APIInstance = {
// GET
get<T>(path: string, config?: AxiosRequestConfig) {
return api.get<T>(path, config).then((res) => res.data);
},
// POST JSON & Form
postJson<T>(path: string, data: any, config?: AxiosRequestConfig) {
return api.post<T>(path, data, config).then((res) => res.data);
},
postForm<T>(path: string, formData: FormData, config?: AxiosRequestConfig) {
return api
.post<T>(path, formData, {
...config,
headers: { ...config?.headers, "Content-Type": "multipart/form-data" },
})
.then((res) => res.data);
},
// PUT JSON & Form
putJson<T>(path: string, data: any, config?: AxiosRequestConfig) {
return api.put<T>(path, data, config).then((res) => res.data);
},
putForm<T>(path: string, formData: FormData, config?: AxiosRequestConfig) {
return api
.put<T>(path, formData, {
...config,
headers: { ...config?.headers, "Content-Type": "multipart/form-data" },
})
.then((res) => res.data);
},
// DELETE (bodyless)
delete<T>(path: string, config?: AxiosRequestConfig) {
return api.delete<T>(path, config).then((res) => res.data);
},
};
export default APIInstance;
Keunggulan & Kekurangan Axios
Berikut adalah analisis keunggulan dan kekurangan penggunaan Axios sebagai HTTP Client default di dalam proyek:
🟢 Keunggulan (Advantages)
- Automatic JSON Transformation: Secara otomatis menserialisasi payload JSON saat mengirim request dan mengurai (parse) data JSON secara otomatis saat menerima response.
- Interceptors (Request & Response): Fitur interceptor yang sangat matang untuk memodifikasi request secara global (seperti menyuntikkan token) atau menangani error secara terpusat (seperti redirect ke halaman login atau melakukan silent refresh token pada status 401).
- Penanganan Error yang Solid: Otomatis melempar error (reject Promise) untuk status code di luar rentang 2xx, dan menyediakan helper typeguards bawaan seperti
axios.isAxiosError. - Dukungan Progress Tracking: Menggunakan
XMLHTTPRequestdi bawah kap yang mendukung fitur pelacakan progress upload/download file secara native dan akurat. - Request Cancellation: Mendukung pembatalan request menggunakan
AbortController(atau legacyCancelToken) untuk menghindari kebocoran memori.
🔴 Kekurangan (Disadvantages)
- Ukuran Bundel (Bundle Size): Memiliki ukuran footprint yang relatif besar (~11.7kB gzipped) dibandingkan native fetch atau client berbasis fetch modern.
- Bukan Native Web Standard: Mengandalkan pustaka pihak ketiga yang harus di-maintain secara berkala di
package.jsonuntuk menghindari celah keamanan. - Runtime Dependency di Edge Environment: Di runtime modern seperti Cloudflare Workers atau Vercel Edge, API berbasis Axios terkadang membutuhkan polyfill karena tidak berjalan sepenuhnya di atas standard Fetch API.
Opsi Alternatif Selain Axios
Jika proyek membutuhkan alternatif HTTP Client yang lebih ringan atau yang mengadopsi standard web modern (Fetch-based), berikut adalah 3 opsi terbaik:
A. Native Fetch API
API bawaan browser modern untuk melakukan HTTP request tanpa bergantung pada pustaka pihak ketiga.
-
Keunggulan (Advantages):
- Zero Dependency: Tidak menambah ukuran bundle size aplikasi klien sama sekali.
- Standard Platform: Didukung secara native di semua browser modern dan runtime server/Edge (Node.js 18+, Bun, Deno, Cloudflare Workers).
- Integrasi Framework: Sangat optimal untuk Server Components (RSC) di Next.js karena integrasi caching bawaan framework berjalan di atas native fetch.
-
Kekurangan (Disadvantages):
- Tanpa Interceptor: Tidak mendukung request/response interceptor bawaan; developer harus membungkus fetch secara manual.
- Error Handling Manual: Tidak melempar error pada status HTTP 4xx atau 5xx. Developer harus mengecek status
response.oksecara manual. - Proses JSON Dua Tahap: Harus memanggil
.json()secara terpisah setelah mendapatkan response (misalnya:const data = await res.json()). - Tidak Ada Request Timeout Bawaan: Membutuhkan penulisan kode manual menggunakan
AbortControlleratauAbortSignal.timeout.
Contoh Implementasi Wrapper Fetch API dengan TypeScript:
interface FetchOptions extends RequestInit {
timeout?: number;
_retry?: boolean;
}
export const nativeClient = {
async request<T>(path: string, options: FetchOptions = {}): Promise<T> {
const { timeout = 15000, _retry = false, ...customOptions } = options;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const config: RequestInit = {
...customOptions,
signal: controller.signal,
credentials: "include", // Cookie Better Auth mengalir otomatis
headers: {
"Content-Type": "application/json",
Accept: "application/json",
...customOptions.headers,
},
};
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${path}`, config);
clearTimeout(id);
// Tangani token expired (401) dan lakukan silent refresh token
if (response.status === 401 && !_retry) {
try {
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`, {
method: "POST",
credentials: "include",
});
// Ulangi request asli secara rekursif
return this.request<T>(path, { ...options, _retry: true });
} catch (refreshError) {
if (typeof window !== "undefined") {
const redirectPath = window.location.pathname + window.location.search;
window.location.href = `/login?message=session_expired&callbackUrl=${encodeURIComponent(redirectPath)}`;
}
throw refreshError;
}
}
if (!response.ok) {
const errData = await response.json().catch(() => ({}));
throw {
message: errData.message || "Terjadi kesalahan pada server",
status: response.status,
code: errData.code || "SERVER_ERROR",
errors: errData.errors || null,
};
}
return await response.json();
} catch (error: any) {
clearTimeout(id);
if (error.name === "AbortError") {
throw { message: "Request timeout", status: 408, code: "TIMEOUT", errors: null };
}
throw error;
}
},
get<T>(path: string, options?: FetchOptions) {
return this.request<T>(path, { ...options, method: "GET" });
},
postJson<T>(path: string, data: any, options?: FetchOptions) {
return this.request<T>(path, { ...options, method: "POST", body: JSON.stringify(data) });
},
};
B. Ky
Pustaka HTTP Client minimalis (~3kB gzipped) yang dibangun langsung di atas native Fetch API, dikembangkan oleh Sindre Sorhus.
-
Keunggulan (Advantages):
- Sangat Ringan: Ukuran bundel hanya sepertiga dari Axios, meminimalkan overhead loading awal.
- Fetch-based & Modern: Bekerja out-of-the-box di runtime Edge dan modern server environments.
- Dukungan Hooks (Interceptors): Memiliki sistem lifecycle hooks seperti
beforeRequestdanafterResponseyang fungsinya mirip interceptor Axios. - Auto-Retry & Timeout: Dilengkapi fitur retry otomatis pada kegagalan koneksi/status code tertentu dan penanganan timeout bawaan.
- Error Handling Otomatis: Otomatis melempar error
HTTPErrorjika status response di luar rentang 2xx.
-
Kekurangan (Disadvantages):
- Membutuhkan Lingkungan Fetch: Membutuhkan runtime yang mendukung Fetch secara native atau polyfill di browser yang sangat jadul.
- Tidak Mendukung Progress Upload: Karena berbasis Fetch, tidak memiliki API native untuk melacak progress upload file sebesar XHR.
Contoh Implementasi Ky Instance dengan TypeScript:
import ky from "ky";
const apiKy = ky.create({
prefixUrl: process.env.NEXT_PUBLIC_API_URL || "https://api.my-app.com/v1",
timeout: 15000,
credentials: "include", // Cookie Better Auth mengalir otomatis
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
hooks: {
beforeRequest: [
(request) => {
request.headers.set("X-Requested-With", "XMLHttpRequest");
},
],
afterResponse: [
async (request, options, response) => {
if (response.status === 401) {
try {
// Panggil endpoint refresh token secara silent
await ky.post(`${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`, {
credentials: "include",
});
// Mengembalikan request baru agar Ky otomatis mengulang request asal
return ky(request);
} catch (refreshError) {
if (typeof window !== "undefined") {
const path = window.location.pathname + window.location.search;
window.location.href = `/login?message=session_expired&callbackUrl=${encodeURIComponent(path)}`;
}
}
}
},
],
},
});
export const kyClient = {
get<T>(path: string) {
return apiKy.get(path).json<T>();
},
postJson<T>(path: string, data: any) {
return apiKy.post(path, { json: data }).json<T>();
},
postForm<T>(path: string, formData: FormData) {
return apiKy.post(path, { body: formData }).json<T>();
},
};
C. Wretch
Wrapper Fetch API yang sangat ringan (~3kB) dengan arsitektur modular (plugin-based) dan sintaksis bergaya fluent/chainable.
-
Keunggulan (Advantages):
- Fluent API: Penulisan chainable yang sangat ekspresif (misalnya:
wretch(url).get().json()). - Penanganan Error Granular: Memiliki method khusus untuk menangani status code tertentu secara langsung (seperti
.unauthorized(),.forbidden(),.notFound()). - Arsitektur Modular: Menyediakan plugin tambahan (retry, middleware, abort) secara terpisah sehingga Anda hanya memuat fitur yang benar-benar digunakan.
- Fluent API: Penulisan chainable yang sangat ekspresif (misalnya:
-
Kekurangan (Disadvantages):
- Sintaksis Berbeda: Menggunakan pola penulisan berantai yang tidak umum bagi kebanyakan developer React/TS.
- Boilerplate untuk Fitur Tambahan: Mengaktifkan fitur seperti retry atau interceptor memerlukan instalasi plugin tambahan dan pendaftaran middleware manual.
Contoh Implementasi Wretch Instance dengan TypeScript:
import wretch from "wretch";
import QueryResolverAddon from "wretch/addons/queryString";
const apiWretch = wretch(process.env.NEXT_PUBLIC_API_URL || "https://api.my-app.com/v1")
.addon(QueryResolverAddon)
.options({
credentials: "include", // Cookie Better Auth mengalir otomatis
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
export const wretchClient = {
get<T>(path: string, query?: Record<string, any>) {
let request = apiWretch.url(path);
if (query) {
request = request.query(query);
}
return request
.get()
.unauthorized(async (error, req) => {
try {
// Panggil endpoint refresh token secara silent
await wretch(`${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`).post().res();
// Mengulang request asal dengan replay()
return req.replay().json();
} catch (refreshError) {
if (typeof window !== "undefined") {
const redirectPath = window.location.pathname + window.location.search;
window.location.href = `/login?message=session_expired&callbackUrl=${encodeURIComponent(redirectPath)}`;
}
throw refreshError;
}
})
.json<T>();
},
postJson<T>(path: string, data: any) {
return apiWretch.url(path).post(data).json<T>();
},
};
Tabel Perbandingan HTTP Clients
| Kriteria | Axios | Native Fetch API | Ky | Wretch |
|---|---|---|---|---|
| Ukuran Bundel | Cukup Berat (~11.7kB) | Zero (0kB - Native) | Sangat Ringan (~3kB) | Sangat Ringan (~3kB) |
| Gaya Sintaksis | Promise & Configuration | Promise / RequestInit | Promise / Fluent | Fluent (Chainable) |
| Edge Runtime Ready | Terbatas (Butuh polyfills) | Ya (Native) | Ya | Ya |
| Interceptors / Hooks | Sangat Matang (Built-in) | Manual / Custom Wrapper | Ya (hooks API) | Ya (via middleware plugin) |
| Penanganan Error | Otomatis (> 2xx) | Manual (response.ok) | Otomatis (HTTPError) | Sangat Granular (per status) |
| Auto-Retry & Timeout | Butuh custom plugin/config | Manual (via AbortController) | Bawaan & Mudah | Ya (via addon plugin) |
| Upload Progress | Ya (Native XHR) | Tidak Ada | Tidak Ada | Tidak Ada |
Panduan Memilih HTTP Client (Decision Guide)
-
Pilihlah Axios jika:
- Proyek Anda membutuhkan tracking upload/download progress yang detail (seperti visualisasi progress bar pengunggahan file secara real-time).
- Tim Anda sangat familiar dengan interceptor Axios dan ingin menggunakan library standar industri yang paling stabil.
-
Pilihlah Native Fetch jika:
- Ukuran bundel aplikasi adalah prioritas mutlak (zero bundle overhead).
- Anda menggunakan Next.js App Router secara intensif dan ingin memaksimalkan performa caching bawaan Next.js di Server Components.
-
Pilihlah Ky jika:
- Anda ingin fitur-fitur Axios (seperti interceptor, timeout, auto-retry, status error throwing) tetapi dalam ukuran bundel yang jauh lebih ringan (~3kB) dan berjalan mulus di Edge Runtime.
-
Pilihlah Wretch jika:
- Anda menyukai penulisan kode asinkronus bergaya functional/fluent yang chainable dan membutuhkan penanganan kode error HTTP (seperti 401, 404, 500) secara terisolasi dan rapi.