Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
22 minLesson 25 of 35
Asynchronous JavaScript

Fetch API & Working with REST APIs

Fetch API & Working with REST APIs

The Fetch API is JavaScript's built-in way to make HTTP requests — no libraries required. It returns Promises, works seamlessly with async/await, and handles everything from simple data fetching to file uploads and streaming responses.

Basic GET Request

// Returns a Promise that resolves to a Response object
const response = await fetch("https://api.example.com/users");

// Response has status info
response.status;    // 200, 404, 500, etc.
response.ok;        // true if status is 200-299
response.headers;   // Headers object

// Parse the body (also returns a Promise)
const data = await response.json();  // parse as JSON
const text = await response.text();  // parse as plain text
const blob = await response.blob();  // parse as binary

Proper Error Handling

fetch only rejects on network errors. A 404 or 500 response resolves successfully — you must check response.ok:

async function getUser(id) {
    const response = await fetch(`/api/users/${id}`);
    
    if (!response.ok) {
        // Try to get error details from the response body
        const error = await response.json().catch(() => ({ message: response.statusText }));
        throw new Error(error.message ?? `HTTP ${response.status}`);
    }
    
    return response.json();
}

// Usage
try {
    const user = await getUser(42);
    displayUser(user);
} catch (err) {
    if (err.message.includes("404")) {
        showNotFound();
    } else {
        showError(err.message);
    }
}

POST, PUT, DELETE Requests

const BASE_URL = "https://api.example.com";

// POST — create a resource
async function createUser(userData) {
    const response = await fetch(`${BASE_URL}/users`, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${getToken()}`
        },
        body: JSON.stringify(userData)
    });
    
    if (!response.ok) throw new Error("Failed to create user");
    return response.json();
}

// PUT — replace a resource
async function updateUser(id, data) {
    const response = await fetch(`${BASE_URL}/users/${id}`, {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data)
    });
    if (!response.ok) throw new Error("Failed to update");
    return response.json();
}

// PATCH — partial update
async function patchUser(id, changes) {
    const response = await fetch(`${BASE_URL}/users/${id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(changes)
    });
    if (!response.ok) throw new Error("Failed to patch");
    return response.json();
}

// DELETE
async function deleteUser(id) {
    const response = await fetch(`${BASE_URL}/users/${id}`, {
        method: "DELETE",
        headers: { "Authorization": `Bearer ${getToken()}` }
    });
    if (!response.ok) throw new Error("Failed to delete");
    // DELETE often returns 204 No Content (no body to parse)
}

Building an API Client

Wrap fetch in a class for reuse:

class ApiClient {
    constructor(baseUrl, getToken) {
        this.baseUrl = baseUrl;
        this.getToken = getToken;
    }
    
    async request(path, options = {}) {
        const url = `${this.baseUrl}${path}`;
        const token = this.getToken?.();
        
        const response = await fetch(url, {
            ...options,
            headers: {
                "Content-Type": "application/json",
                ...(token && { "Authorization": `Bearer ${token}` }),
                ...options.headers
            }
        });
        
        if (response.status === 204) return null;  // No Content
        
        const data = await response.json().catch(() => null);
        
        if (!response.ok) {
            const message = data?.message ?? `HTTP ${response.status}`;
            const error = new Error(message);
            error.status = response.status;
            error.data = data;
            throw error;
        }
        
        return data;
    }
    
    get(path, params = {}) {
        const query = new URLSearchParams(params).toString();
        return this.request(`${path}${query ? `?${query}` : ""}`);
    }
    
    post(path, body) {
        return this.request(path, { method: "POST", body: JSON.stringify(body) });
    }
    
    put(path, body) {
        return this.request(path, { method: "PUT", body: JSON.stringify(body) });
    }
    
    patch(path, body) {
        return this.request(path, { method: "PATCH", body: JSON.stringify(body) });
    }
    
    delete(path) {
        return this.request(path, { method: "DELETE" });
    }
}

// Usage
const api = new ApiClient("https://api.example.com", () => localStorage.getItem("token"));

const users = await api.get("/users", { page: 1, limit: 20 });
const newUser = await api.post("/users", { name: "Alice", email: "alice@example.com" });
await api.delete(`/users/${id}`);

Query Parameters

// URLSearchParams for clean query string building
const params = new URLSearchParams({
    page: 1,
    limit: 20,
    sort: "name",
    filter: "active"
});

params.toString();  // "page=1&limit=20&sort=name&filter=active"

fetch(`/api/users?${params}`);

// Append multiple values for same key
const tags = new URLSearchParams();
["javascript", "react", "nextjs"].forEach(tag => tags.append("tag", tag));
// "tag=javascript&tag=react&tag=nextjs"

File Upload

// FormData for multipart/form-data uploads
const fileInput = document.querySelector("#file");

async function uploadFile(file) {
    const formData = new FormData();
    formData.append("file", file);
    formData.append("category", "avatar");
    
    // Don't set Content-Type header — browser sets it with boundary
    const response = await fetch("/api/upload", {
        method: "POST",
        body: formData
    });
    
    if (!response.ok) throw new Error("Upload failed");
    return response.json();
}

AbortController — Cancellable Requests

// Cancel a request in progress
const controller = new AbortController();

// Cancel if user navigates away
setTimeout(() => controller.abort(), 5000);

try {
    const response = await fetch("/api/slow-endpoint", {
        signal: controller.signal
    });
    const data = await response.json();
} catch (err) {
    if (err.name === "AbortError") {
        console.log("Request cancelled");
    } else {
        throw err;
    }
}

// React pattern: cancel on component unmount
useEffect(() => {
    const controller = new AbortController();
    
    fetch("/api/data", { signal: controller.signal })
        .then(r => r.json())
        .then(setData)
        .catch(err => {
            if (err.name !== "AbortError") setError(err);
        });
    
    return () => controller.abort();  // cleanup
}, []);

Next lesson: Classes & Prototype Chain — understanding JavaScript's object-oriented system.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!