1. Pengenalan Fetch API
Fetch API adalah cara modern untuk melakukan HTTP requests di JavaScript. Fetch menggantikan XMLHttpRequest (XHR) yang sudah tua dan menyediakan API yang lebih bersih, berbasis Promises, dan lebih powerful.
Fetch vs XMLHttpRequest
| Fitur | Fetch API | XMLHttpRequest |
|---|---|---|
| Syntax | Modern, clean, Promise-based | Callback-based, verbose |
| Promises | ✅ Ya | ❌ Tidak (butuh wrapper) |
| Async/Await | ✅ Ya | ❌ Tidak langsung |
| Streaming | ✅ Ya (ReadableStream) | ❌ Terbatas |
| Request Cancellation | ✅ AbortController | ✅ .abort() |
| Progress Upload | ❌ Tidak (butuh XHR) | ✅ Ya |
| CORS | ✅ Lebih baik | ⚠️ Masalah di browser lama |
// ===== Fetch API Dasar =====
// fetch() mengembalikan Promise yang resolve ke Response object
// Contoh paling sederhana
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// Dengan async/await (lebih direkomendasikan)
async function ambilData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
// ===== Perbedaan dengan XHR =====
// XMLHttpRequest (cara lama):
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data');
xhr.onload = function() {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
}
};
xhr.onerror = function() { console.error('Error'); };
xhr.send();
// Fetch (cara modern — lebih singkat!):
const data = await (await fetch('https://api.example.com/data')).json();
console.log(data);
Fetch hanya reject promise-nya saat network error (tidak ada internet, DNS failure). HTTP status seperti 404 atau 500 bukan error untuk fetch — promise tetap resolve. Anda harus mengecek response.ok secara manual.
2. GET Request
// ===== GET Request Sederhana =====
async function getUsers() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await response.json();
console.log(`Ditemukan ${users.length} users`);
return users;
}
// ===== GET dengan Query Parameters =====
async function searchUsers(query) {
const params = new URLSearchParams({
q: query,
limit: 10,
offset: 0
});
const url = `https://api.example.com/users?${params}`;
const response = await fetch(url);
return await response.json();
}
// Atau manual:
const url = new URL('https://api.example.com/users');
url.searchParams.append('q', 'beebane');
url.searchParams.append('page', '1');
console.log(url.toString());
// → https://api.example.com/users?q=beebane&page=1
// ===== GET dengan Headers =====
async function getProfile(userId) {
const response = await fetch(`/api/users/${userId}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1...',
'Accept': 'application/json',
'X-Custom-Header': 'custom-value'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
}
// ===== Membaca Response Berbagai Format =====
async function ambilData(url) {
const response = await fetch(url);
// JSON
const jsonData = await response.json();
// Text
// const textData = await response.text();
// Binary (Blob)
// const blobData = await response.blob();
// ArrayBuffer (binary raw)
// const bufferData = await response.arrayBuffer();
// FormData
// const formData = await response.formData();
return jsonData;
}
3. POST, PUT, DELETE
// ===== POST: Mengirim Data ke Server =====
async function createUser(userData) {
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
nama: userData.nama,
email: userData.email,
role: 'member'
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Gagal membuat user');
}
const newUser = await response.json();
console.log('User dibuat:', newUser);
return newUser;
}
// Penggunaan:
try {
const user = await createUser({
nama: 'Beebane',
email: 'bee@example.com'
});
console.log('ID user baru:', user.id);
} catch (error) {
console.error('Gagal:', error.message);
}
// ===== POST dengan Form Data =====
async function uploadFile(fileInput) {
const formData = new FormData();
formData.append('avatar', fileInput.files[0]);
formData.append('nama', 'Beebane');
formData.append('bio', 'Web Developer');
// TIDAK perlu set Content-Type — browser otomatis set
// dengan boundary yang benar untuk multipart/form-data
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
return await response.json();
}
// ===== POST dengan URL Encoded Form =====
async function login(username, password) {
const params = new URLSearchParams();
params.append('username', username);
params.append('password', password);
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params
});
return await response.json();
}
// ===== PUT: Update Seluruh Resource =====
async function updateUser(userId, userData) {
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
return await response.json();
}
// ===== PATCH: Update Sebagian Resource =====
async function patchUser(userId, changes) {
const response = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(changes)
});
return await response.json();
}
// Hanya update email saja:
await patchUser(1, { email: 'baru@example.com' });
// ===== DELETE: Hapus Resource =====
async function deleteUser(userId) {
const response = await fetch(`/api/users/${userId}`, {
method: 'DELETE'
});
if (response.status === 204) {
console.log('User berhasil dihapus');
return true;
}
if (!response.ok) {
throw new Error('Gagal menghapus user');
}
return true;
}
// ===== Menggunakan Semua HTTP Methods =====
const API = {
baseUrl: 'https://jsonplaceholder.typicode.com',
async get(path) {
const res = await fetch(`${this.baseUrl}${path}`);
return res.json();
},
async post(path, data) {
const res = await fetch(`${this.baseUrl}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return res.json();
},
async put(path, data) {
const res = await fetch(`${this.baseUrl}${path}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return res.json();
},
async delete(path) {
const res = await fetch(`${this.baseUrl}${path}`, {
method: 'DELETE'
});
return res.ok;
}
};
// Penggunaan:
const posts = await API.get('/posts');
const newPost = await API.post('/posts', { title: 'Halo', body: 'Isi' });
await API.put(`/posts/${newPost.id}`, { title: 'Updated' });
await API.delete(`/posts/${newPost.id}`);
4. Headers & Options
// ===== Headers API =====
// Membuat Headers dari object literal
const headers = new Headers({
'Content-Type': 'application/json',
'Authorization': 'Bearer token-abc',
'Accept': 'application/json',
'X-Request-ID': crypto.randomUUID()
});
// Menambah header
headers.append('X-Custom', 'value');
// Membaca header
console.log(headers.get('Content-Type')); // → 'application/json'
// Cek header
console.log(headers.has('Authorization')); // → true
// Hapus header
headers.delete('X-Custom');
// Iterasi headers
for (const [key, value] of headers) {
console.log(`${key}: ${value}`);
}
// ===== Request Object (Alternatif) =====
// Bisa buat Request object terlebih dahulu
const request = new Request('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token-abc'
},
body: JSON.stringify({ key: 'value' }),
mode: 'cors', // 'cors', 'no-cors', 'same-origin'
credentials: 'same-origin', // 'omit', 'same-origin', 'include'
cache: 'no-cache', // 'default', 'no-cache', 'reload', etc.
redirect: 'follow', // 'follow', 'error', 'manual'
referrerPolicy: 'no-referrer',
signal: null, // AbortSignal
keepalive: false // Kirim meski halaman ditutup
});
const response = await fetch(request);
// ===== Complete Fetch Options =====
const fullOptions = {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
'Cache-Control': 'no-cache',
'X-API-Key': 'your-api-key'
}),
body: JSON.stringify({ action: 'update' }),
mode: 'cors',
credentials: 'include',
cache: 'no-cache',
redirect: 'follow',
referrer: 'https://example.com/page',
referrerPolicy: 'strict-origin-when-cross-origin',
keepalive: false
};
const response2 = await fetch('/api/action', fullOptions);
5. Response Object
// ===== Response Properties =====
const response = await fetch('https://api.example.com/data');
// Status
console.log(response.status); // 200, 404, 500, dll
console.log(response.statusText); // 'OK', 'Not Found', 'Internal Server Error'
console.log(response.ok); // true jika status 200-299
// Headers
console.log(response.headers.get('content-type')); // 'application/json'
console.log(response.headers.get('x-request-id'));
// Metadata
console.log(response.url); // Final URL (setelah redirect)
console.log(response.redirected); // true jika dialihkan
console.log(response.type); // 'basic', 'cors', 'opaque'
console.log(response.bodyUsed); // true jika body sudah dibaca
// ===== Membaca Body (hanya bisa sekali!) =====
// ⚠️ Body hanya bisa dibaca SATU KALI!
// Jika perlu membaca ulang, clone response-nya
// JSON
const jsonResponse = await fetch('/api/data');
const jsonData = await jsonResponse.json();
console.log(jsonData);
// Text
const textResponse = await fetch('/page.html');
const html = await textResponse.text();
console.log(html);
// Blob (binary — gambar, file)
const imgResponse = await fetch('/images/photo.jpg');
const imgBlob = await imgResponse.blob();
const imgUrl = URL.createObjectURL(imgBlob);
document.querySelector('img').src = imgUrl;
// ArrayBuffer (raw binary)
const audioResponse = await fetch('/audio.mp3');
const audioBuffer = await audioResponse.arrayBuffer();
// ===== Clone Response =====
const original = await fetch('/api/data');
// Clone sebelum membaca
const clone1 = original.clone();
const clone2 = original.clone();
const json = await original.json(); // ✅ OK
const text = await clone1.text(); // ✅ OK
const blob = await clone2.blob(); // ✅ OK
// ===== Custom Response =====
const customResponse = new Response(
JSON.stringify({ message: 'Hello from Service Worker!' }),
{
status: 200,
statusText: 'OK',
headers: new Headers({
'Content-Type': 'application/json',
'X-Custom': 'custom-value'
})
}
);
6. Error Handling
// ===== Pola Error Handling yang Robust =====
class APIError extends Error {
constructor(message, status, response) {
super(message);
this.name = 'APIError';
this.status = status;
this.response = response;
}
}
async function apiRequest(url, options = {}) {
let response;
try {
response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
} catch (error) {
// Network error — tidak ada internet, DNS failure, dll
throw new APIError(
'Tidak bisa terhubung ke server. Cek koneksi internet Anda.',
0, // status 0 = network error
null
);
}
// HTTP error — 4xx, 5xx
if (!response.ok) {
let errorMessage;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error;
} catch {
errorMessage = response.statusText;
}
switch (response.status) {
case 400:
throw new APIError(
errorMessage || 'Request tidak valid', 400, response
);
case 401:
throw new APIError(
'Sesi habis. Silakan login kembali.', 401, response
);
case 403:
throw new APIError(
'Anda tidak memiliki akses.', 403, response
);
case 404:
throw new APIError(
'Data tidak ditemukan.', 404, response
);
case 429:
throw new APIError(
'Terlalu banyak request. Coba lagi nanti.', 429, response
);
case 500:
case 502:
case 503:
throw new APIError(
'Server sedang bermasalah. Coba lagi nanti.',
response.status, response
);
default:
throw new APIError(
errorMessage || `Error: ${response.status}`,
response.status, response
);
}
}
// Success — parse response
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return await response.text();
}
// Penggunaan:
try {
const users = await apiRequest('/api/users');
console.log(users);
} catch (error) {
if (error instanceof APIError) {
if (error.status === 401) {
window.location.href = '/login';
} else {
showErrorNotification(error.message);
}
}
}
7. AbortController
AbortController memungkinkan Anda membatalkan fetch request yang sedang berjalan — sangat penting untuk mencegah race condition, membatalkan search saat user mengetik, dan timeout.
// ===== Abort Fetch yang Sedang Berjalan =====
// 1. Abort Manual
const controller = new AbortController();
const { signal } = controller;
fetch('/api/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch dibatalkan!');
} else {
console.error('Error:', error);
}
});
// Batalkan setelah 2 detik
setTimeout(() => controller.abort(), 2000);
// ===== 2. Fetch dengan Timeout =====
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const { signal } = controller;
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, { ...options, signal })
.then(response => {
clearTimeout(timeoutId);
return response;
});
}
// Penggunaan:
try {
const response = await fetchWithTimeout('/api/data', {}, 3000);
const data = await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.error('Request timeout setelah 3 detik!');
}
}
// ===== 3. Debounce Search dengan Abort =====
let searchController = null;
async function searchAPI(query) {
// Batalkan request sebelumnya jika ada
if (searchController) {
searchController.abort();
}
// Buat controller baru
searchController = new AbortController();
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: searchController.signal
});
const results = await response.json();
displayResults(results);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Search sebelumnya dibatalkan');
} else {
console.error('Search error:', error);
}
}
}
// Event listener — batalkan fetch lama saat user mengetik
document.getElementById('searchInput').addEventListener('input', (e) => {
searchAPI(e.target.value);
});
// ===== 4. Abort Beberapa Fetch Sekaligus =====
async function fetchMultiple(urls) {
const controller = new AbortController();
const { signal } = controller;
const promises = urls.map(url =>
fetch(url, { signal }).then(r => r.json())
);
// Timeout untuk semua
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const results = await Promise.all(promises);
clearTimeout(timeoutId);
return results;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
// ===== 5. AbortSignal.timeout() — Shortcut =====
// API baru (tidak semua browser support)
try {
const response = await fetch('/api/data', {
signal: AbortSignal.timeout(5000) // Timeout 5 detik
});
const data = await response.json();
} catch (error) {
if (error.name === 'TimeoutError') {
console.error('Timeout!');
}
}
// ===== 6. AbortSignal.any() — Gabungkan Signals =====
// Jika salah satu signal abort, fetch dibatalkan
const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(10000);
try {
const response = await fetch('/api/data', {
signal: AbortSignal.any([
userController.signal,
timeoutSignal
])
});
} catch (error) {
console.log('Fetch dibatalkan:', error.name);
}
8. Streaming Response
Fetch mendukung streaming — Anda bisa membaca response secara bertahap tanpa menunggu seluruh data selesai. Ini sangat berguna untuk file besar atau streaming API (seperti ChatGPT).
// ===== Membaca Stream dengan ReadableStream =====
async function streamResponse(url) {
const response = await fetch(url);
// Dapatkan reader dari body stream
const reader = response.body.getReader();
const contentLength = response.headers.get('Content-Length');
const total = parseInt(contentLength, 10);
let receivedLength = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
// Update progress bar
if (total) {
const percent = Math.round((receivedLength / total) * 100);
updateProgress(percent);
console.log(`Diterima: ${receivedLength} dari ${total} bytes (${percent}%)`);
}
}
// Gabungkan semua chunks
const allChunks = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
allChunks.set(chunk, position);
position += chunk.length;
}
return allChunks;
}
// ===== Stream Text (seperti ChatGPT/LLM API) =====
async function streamText(prompt) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
const output = document.getElementById('output');
output.textContent = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
output.textContent += text;
// Auto-scroll ke bawah
output.scrollTop = output.scrollHeight;
}
console.log('Streaming selesai!');
}
// ===== Download dengan Progress =====
async function downloadWithProgress(url, filename) {
const response = await fetch(url);
const contentLength = response.headers.get('Content-Length');
const total = parseInt(contentLength, 10) || 0;
const reader = response.body.getReader();
const chunks = [];
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
// Update UI
if (total > 0) {
const percent = ((received / total) * 100).toFixed(1);
document.getElementById('progress').textContent =
`${percent}% (${(received / 1024 / 1024).toFixed(1)} MB)`;
}
}
// Buat blob dan download
const blob = new Blob(chunks);
const url2 = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url2;
a.download = filename;
a.click();
URL.revokeObjectURL(url2);
}
// ===== TransformStream: Proses Data Streaming =====
async function processStream(url) {
const response = await fetch(url);
// Buat transform stream untuk mengubah huruf besar
const transform = new TransformStream({
transform(chunk, controller) {
const text = new TextDecoder().decode(chunk);
controller.enqueue(
new TextEncoder().encode(text.toUpperCase())
);
}
});
const transformedStream = response.body.pipeThrough(transform);
const reader = transformedStream.getReader();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += new TextDecoder().decode(value);
}
return result;
}
9. Pola HTTP Modern
HTTP Client Class
// ===== Reusable HTTP Client =====
class HttpClient {
constructor(baseUrl = '') {
this.baseUrl = baseUrl;
this.defaultHeaders = {};
this.interceptors = { request: [], response: [] };
}
setHeader(key, value) {
this.defaultHeaders[key] = value;
}
addRequestInterceptor(fn) {
this.interceptors.request.push(fn);
}
addResponseInterceptor(fn) {
this.interceptors.response.push(fn);
}
async request(endpoint, options = {}) {
let url = `${this.baseUrl}${endpoint}`;
let config = {
...options,
headers: {
...this.defaultHeaders,
...options.headers
}
};
// Jalankan request interceptors
for (const interceptor of this.interceptors.request) {
config = await interceptor(config);
}
let response = await fetch(url, config);
// Jalankan response interceptors
for (const interceptor of this.interceptors.response) {
response = await interceptor(response);
}
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw { status: response.status, ...error };
}
return response.json();
}
get(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'GET' });
}
post(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
}
put(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'PUT',
body: JSON.stringify(data)
});
}
delete(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'DELETE' });
}
}
// Penggunaan:
const api = new HttpClient('https://jsonplaceholder.typicode.com');
api.setHeader('Content-Type', 'application/json');
// Tambah token otomatis
api.addRequestInterceptor(async (config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
});
const posts = await api.get('/posts');
const newPost = await api.post('/posts', { title: 'Test', body: 'Isi' });
Retry Pattern
// ===== Retry dengan Exponential Backoff =====
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
// Jangan retry untuk error client (4xx)
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client Error: ${response.status}`);
}
// Server error (5xx) → bisa di-retry
lastError = new Error(`Server Error: ${response.status}`);
} catch (error) {
lastError = error;
// Jangan retry untuk error yang bukan network/server
if (error.message.startsWith('Client Error')) {
throw error;
}
}
// Tunggu sebelum retry (exponential backoff)
if (attempt < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
const jitter = delay * 0.5 * Math.random();
const waitTime = delay + jitter;
console.log(
`Retry ${attempt + 1}/${maxRetries} dalam ${waitTime.toFixed(0)}ms...`
);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
throw lastError;
}
// Penggunaan:
try {
const response = await fetchWithRetry('/api/data', {}, 3);
const data = await response.json();
} catch (error) {
console.error('Gagal setelah 3 kali retry:', error);
}
10. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut: