Skip to content
JavaScript Fetch API Explained — GET, POST, PUT, DELETE & CRUD with REST

JavaScript Fetch API Explained — GET, POST, PUT, DELETE & CRUD with REST

DodaTech Updated Jun 15, 2026 8 min read

The Fetch API is the modern JavaScript interface for making HTTP requests, replacing the older XMLHttpRequest. It’s built into every modern browser and provides a clean, promise-based way to communicate with servers. The Doda Browser uses Fetch for everything from loading web pages to syncing bookmarks, and the Durga Antivirus Pro dashboard uses it to fetch threat data and send scan results.

What You’ll Learn

  • Making GET, POST, PUT, and DELETE requests with Fetch
  • Setting headers, sending JSON, and handling responses
  • Uploading files with FormData
  • Cancelling requests with AbortController
  • Error handling — network errors vs HTTP errors
  • Building request interceptors for logging and auth
  • A complete CRUD example against a REST API

Why Fetch Matters

Every modern web application communicates with a server. Whether you’re loading user data, submitting a form, or uploading a file, you need a reliable way to make HTTP requests. Fetch is simpler, more powerful, and more consistent than the old XMLHttpRequest — and it integrates naturally with promises and async/await.

Learning Path

    flowchart LR
  A[Async JavaScript] --> B[Fetch API]
  B --> C[Promises Deep Dive]
  B --> D[Browser Storage]
  B --> E[You Are Here]
  
Prerequisites: You should understand JavaScript promises and Async Await syntax. Basic knowledge of REST APIs and JSON is helpful.

Basic GET Request

The simplest Fetch call — a GET request to retrieve data:

async function getUsers() {
  try {
    const response = await fetch('https://api.example.com/users');

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const users = await response.json();
    console.log(users);
    return users;
  } catch (error) {
    console.error('Failed to fetch users:', error);
    return [];
  }
}

getUsers();

Critical: Fetch only rejects on network errors (DNS failure, no connection, etc.). HTTP errors like 404 or 500 are treated as successful responses. Always check response.ok.

POST — Sending Data

async function createUser(userData) {
  const response = await fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getToken()}`
    },
    body: JSON.stringify(userData)
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(error.message || `HTTP ${response.status}`);
  }

  return response.json();
}

const newUser = await createUser({
  name: 'Alice',
  email: 'alice@example.com'
});

PUT — Updating Data

async function updateUser(id, updates) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(updates)
  });

  if (!response.ok) throw new Error(`Update failed: ${response.status}`);
  return response.json();
}

DELETE — Removing Data

async function deleteUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: 'DELETE'
  });

  if (!response.ok) throw new Error(`Delete failed: ${response.status}`);

  // 204 No Content — no body to parse
  if (response.status === 204) return { success: true };

  return response.json();
}

Working with Headers

The Headers API gives you full control over request and response headers:

// Create headers
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Authorization', `Bearer ${token}`);
headers.set('X-Request-ID', crypto.randomUUID());

// Headers are iterable
for (const [name, value] of headers) {
  console.log(`${name}: ${value}`);
}

// Use with fetch
const response = await fetch('/api/data', { headers });
console.log(response.headers.get('X-RateLimit-Remaining'));

File Upload with FormData

async function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);
  formData.append('description', 'User avatar');

  const response = await fetch('/api/upload', {
    method: 'POST',
    // Don't set Content-Type — browser sets it with boundary
    body: formData
  });

  if (!response.ok) throw new Error('Upload failed');
  return response.json();
}

// Usage
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const result = await uploadFile(file);
  console.log('Uploaded:', result.url);
});

Note: When sending FormData, don’t set Content-Type manually. The browser sets it automatically with the correct multipart/form-data boundary.

AbortController — Cancelling Requests

function fetchWithTimeout(url, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);

  return fetch(url, { signal: controller.signal })
    .finally(() => clearTimeout(timeout));
}

// Usage
try {
  const data = await fetchWithTimeout('https://api.example.com/data', 3000);
  console.log(data);
} catch (error) {
  if (error.name === 'AbortError') {
    console.error('Request timed out');
  } else {
    console.error('Request failed:', error);
  }
}

// Cancelling on user action
const controller = new AbortController();
document.getElementById('cancelBtn').onclick = () => controller.abort();

fetch('/api/large-data', { signal: controller.signal })
  .then(r => r.json())
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Request cancelled by user');
    }
  });

Error Handling Patterns

class ApiError extends Error {
  constructor(status, message, data) {
    super(message);
    this.name = 'ApiError';
    this.status = status;
    this.data = data;
  }
}

async function apiRequest(url, options = {}) {
  try {
    const response = await fetch(url, {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      },
      ...options
    });

    // Handle non-JSON responses
    const contentType = response.headers.get('content-type');
    const isJson = contentType?.includes('application/json');

    const data = isJson ? await response.json() : null;

    if (!response.ok) {
      throw new ApiError(
        response.status,
        data?.message || `HTTP ${response.status}`,
        data
      );
    }

    return data;
  } catch (error) {
    if (error instanceof ApiError) throw error;
    throw new ApiError(0, 'Network error — check your connection');
  }
}

Request Interceptors

Interceptors let you modify requests or responses globally — perfect for adding auth tokens, logging, or retry logic:

function createApiClient(baseURL) {
  const interceptors = [];

  function use(interceptor) {
    interceptors.push(interceptor);
  }

  async function request(endpoint, options = {}) {
    let config = {
      url: `${baseURL}${endpoint}`,
      ...options,
      headers: { 'Content-Type': 'application/json', ...options.headers }
    };

    // Run request interceptors
    for (const interceptor of interceptors) {
      if (interceptor.request) {
        config = await interceptor.request(config);
      }
    }

    try {
      let response = await fetch(config.url, config);

      // Run response interceptors
      for (const interceptor of interceptors) {
        if (interceptor.response) {
          response = await interceptor.response(response);
        }
      }

      return response.json();
    } catch (error) {
      throw error;
    }
  }

  return { use, request };
}

// Usage
const api = createApiClient('https://api.example.com');

api.use({
  request: async (config) => {
    config.headers.Authorization = `Bearer ${getToken()}`;
    console.log(`[${config.method}] ${config.url}`);
    return config;
  },
  response: async (response) => {
    if (response.status === 401) {
      // Redirect to login
      window.location.href = '/login';
    }
    return response;
  }
});

const users = await api.request('/users');

Full CRUD Example

class UserService {
  constructor(baseURL = 'https://api.example.com/users') {
    this.baseURL = baseURL;
  }

  async getAll() {
    return apiRequest(this.baseURL);
  }

  async getById(id) {
    return apiRequest(`${this.baseURL}/${id}`);
  }

  async create(userData) {
    return apiRequest(this.baseURL, {
      method: 'POST',
      body: JSON.stringify(userData)
    });
  }

  async update(id, updates) {
    return apiRequest(`${this.baseURL}/${id}`, {
      method: 'PUT',
      body: JSON.stringify(updates)
    });
  }

  async delete(id) {
    return apiRequest(`${this.baseURL}/${id}`, {
      method: 'DELETE'
    });
  }
}

const users = new UserService();
await users.create({ name: 'Bob', email: 'bob@example.com' });
const allUsers = await users.getAll();

Common Mistakes

1. Not checking response.ok

const res = await fetch('/api/data');
const data = await res.json(); // Throws if 404 returns HTML

Fix: Always check response.ok before parsing.

2. Forgetting to parse JSON

const data = await fetch('/api/data'); // Returns Response object

Fix: Call response.json() to parse the body.

3. CORS errors

Making requests to a different origin without proper CORS headers. Fix: Ensure the server sends Access-Control-Allow-Origin headers.

4. Not handling network errors

const data = await fetch('/api/data').then(r => r.json());
// If offline, this throws an unhandled rejection

Fix: Always wrap in try/catch.

5. Setting Content-Type with FormData

fetch('/api/upload', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' }, // ❌
  body: formData
});

Fix: Let the browser set the content type for FormData.

Practice Questions

  1. What’s the difference between a network error and an HTTP error in Fetch? Network errors (DNS failure, no connection) cause Fetch to reject. HTTP errors (404, 500) are treated as successful responses — you must check response.ok.

  2. How do you cancel a fetch request? Use an AbortController. Pass its signal to the request and call controller.abort() to cancel.

  3. Why shouldn’t you set Content-Type for FormData? The browser needs to set the correct multipart/form-data boundary. Setting it manually breaks the upload.

  4. What does response.json() return? A Promise that resolves to the parsed JSON body. You must await it or use .then().

Challenge: Build a function fetchWithRetry(url, options, maxRetries) that automatically retries failed requests with exponential backoff.

Mini Project: REST API Client

async function createPost() {
  const newPost = {
    title: 'My New Post',
    body: 'This is the content of my post.',
    userId: 1
  };

  const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    body: JSON.stringify(newPost),
    headers: {
      'Content-type': 'application/json; charset=UTF-8',
    },
  });

  const post = await response.json();
  console.log('Created post:', post);
  // Output: Created post: { id: 101, title: 'My New Post', ... }
}

FAQ

What is the difference between Fetch and XMLHttpRequest?
Fetch is promise-based, cleaner, and built into modern browsers. XMLHttpRequest is older, callback-based, and more verbose.
Does Fetch support upload progress?
Not natively. Use XMLHttpRequest or the fetch progress polyfill for upload progress tracking.
How do I send credentials with Fetch?
Add credentials: 'include' to send cookies cross-origin, or credentials: 'same-origin' for same-origin requests.
What is CORS and why does it matter?
CORS (Cross-Origin Resource Sharing) is a security mechanism that controls which origins can access resources on your server.
Can I use Fetch in Node.js?
Fetch is available in Node.js 18+ natively. For older versions, use libraries like node-fetch or axios.

What’s Next

LessonDescription
JavaScript HomeBack to the JavaScript hub
https://tutorials.dodatech.com/programming-languages/javascript/js-async/Async JavaScript fundamentals
https://tutorials.dodatech.com/programming-languages/javascript/js-promises-deep/Promises deep dive
https://tutorials.dodatech.com/programming-languages/javascript/js-error-handling/Error handling patterns
REST APIRESTful API design

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

What’s Next

Congratulations on completing this JavaScript Fetch API tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro