Fetch API

Fetch API — современный интерфейс для выполнения HTTP-запросов, возвращающий Promise.

Зачем нужно

Fetch — стандартный способ общения с сервером из браузера. Заменил XMLHttpRequest. Используется для загрузки данных, отправки форм, работы с REST API.

Где используется

  • AJAX-запросы
  • REST API (CRUD-операции)
  • Загрузка/отправка файлов
  • Работа с GraphQL
  • Server-Sent Events

Предпосылки

Promise, async-await, JSON

GET запрос

// Простейший GET
const response = await fetch('https://api.example.com/users');
const users = await response.json();

// Проверка статуса (fetch НЕ бросает ошибку при 404/500!)
async function fetchJSON(url) {
  const response = await fetch(url);

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

  return response.json();
}

// С query-параметрами
const params = new URLSearchParams({
  page: 1,
  limit: 10,
  search: 'алиса'
});
const response = await fetch(`/api/users?${params}`);

POST запрос

// Отправка JSON
const response = await fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Алиса',
    email: 'alice@mail.com'
  })
});

const newUser = await response.json();
console.log('Создан пользователь:', newUser);

PUT / PATCH / DELETE

// PUT — полная замена
await fetch('/api/users/1', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Боб', email: 'bob@mail.com' })
});

// PATCH — частичное обновление
await fetch('/api/users/1', {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Боб' })
});

// DELETE
await fetch('/api/users/1', {
  method: 'DELETE'
});

Объект Response

const response = await fetch('/api/data');

// Свойства
response.ok;          // true если статус 200-299
response.status;      // 200, 404, 500, и т.д.
response.statusText;  // "OK", "Not Found"
response.url;         // URL ответа (после редиректов)
response.redirected;  // был ли редирект
response.headers;     // объект Headers

// Методы чтения тела (можно вызвать ТОЛЬКО ОДИН раз!)
await response.json();        // парсит как JSON
await response.text();        // как строку
await response.blob();        // как Blob (файлы)
await response.arrayBuffer(); // как ArrayBuffer
await response.formData();    // как FormData

// Клонирование для повторного чтения
const clone = response.clone();
const text = await clone.text();
const json = await response.json();

Headers

// Создание
const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Authorization', 'Bearer token123');

// Или объектом
const response = await fetch('/api', {
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123',
    'X-Custom-Header': 'value'
  }
});

// Чтение заголовков ответа
response.headers.get('Content-Type');     // 'application/json'
response.headers.has('Authorization');     // false (серверный)

for (const [key, value] of response.headers) {
  console.log(`${key}: ${value}`);
}

Отправка FormData

// Из HTML-формы
const form = document.querySelector('#myForm');
const formData = new FormData(form);

await fetch('/api/submit', {
  method: 'POST',
  body: formData // Content-Type установится автоматически
});

// Вручную
const formData = new FormData();
formData.append('name', 'Алиса');
formData.append('avatar', fileInput.files[0]);

await fetch('/api/upload', {
  method: 'POST',
  body: formData
});

Обработка ошибок

async function safeFetch(url, options = {}) {
  try {
    const response = await fetch(url, options);

    if (!response.ok) {
      // Попробуем получить сообщение об ошибке из тела
      const errorBody = await response.json().catch( => null);
      throw new FetchError(
        errorBody?.message || `HTTP ${response.status}`,
        response.status,
        errorBody
      );
    }

    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      // Сетевая ошибка (нет интернета, CORS, DNS)
      throw new Error('Сетевая ошибка: проверьте подключение');
    }
    throw error;
  }
}

// Кастомный класс ошибки
class FetchError extends Error {
  constructor(message, status, body) {
    super(message);
    this.name = 'FetchError';
    this.status = status;
    this.body = body;
  }
}

// Использование
try {
  const data = await safeFetch('/api/users');
} catch (error) {
  if (error instanceof FetchError) {
    if (error.status === 404) console.log('Не найдено');
    if (error.status === 401) redirectToLogin;
  }
}

AbortController (отмена запроса)

// Создаём контроллер
const controller = new AbortController();

// Передаём signal в fetch
fetch('/api/data', { signal: controller.signal })
  .then(r => r.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Запрос отменён');
    }
  });

// Отменяем запрос
controller.abort();

// Таймаут с AbortController
async function fetchWithTimeout(url, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout( => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    return await response.json();
  } finally {
    clearTimeout(timeoutId);
  }
}

// Встроенный таймаут (новый API)
const response = await fetch('/api/data', {
  signal: AbortSignal.timeout(5000)
});

Практические паттерны

CRUD-обёртка

const api = {
  baseURL: '/api',

  async get(path) {
    const res = await fetch(`${this.baseURL}${path}`);
    if (!res.ok) throw new Error(`GET ${path}: ${res.status}`);
    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)
    });
    if (!res.ok) throw new Error(`POST ${path}: ${res.status}`);
    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)
    });
    if (!res.ok) throw new Error(`PUT ${path}: ${res.status}`);
    return res.json();
  },

  async delete(path) {
    const res = await fetch(`${this.baseURL}${path}`, { method: 'DELETE' });
    if (!res.ok) throw new Error(`DELETE ${path}: ${res.status}`);
    return res.ok;
  }
};

// Использование
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'Алиса' });
await api.delete('/users/1');

Частые ошибки

1. fetch не бросает ошибку при 404/500

// fetch бросает ошибку ТОЛЬКО при сетевых проблемах
// 404, 500 — это "успешные" ответы для fetch

const response = await fetch('/api/not-found');
console.log(response.ok);     // false
console.log(response.status); // 404
// Ошибки нет! Нужно проверять вручную

2. Двойное чтение body

const response = await fetch('/api');
const text = await response.text();
// const json = await response.json(); // TypeError: body already read

// Решение: клонировать
const clone = response.clone();

3. Забыт Content-Type для POST

// Сервер не поймёт JSON без заголовка
await fetch('/api', {
  method: 'POST',
  // headers: { 'Content-Type': 'application/json' }, // Забыли!
  body: JSON.stringify(data) // Сервер получит как text/plain
});

Практика

  1. Реализуй CRUD для сущности "задача" (todo)
  2. Загрузи данные из 3 разных API параллельно
  3. Реализуй retry с экспоненциальной задержкой
  4. Добавь таймаут 5 секунд с AbortController

Связанные темы

Ресурсы


🎓 Источник: Axios и Fetch API в современном Node.js

  • 📅 2025-02-11 · YouTube
  • Тезисы:
    • Axios устарел. Node 18+ имеет нативный fetch на базе undici.
    • node-fetch тоже больше не нужен — это полифил, когда fetch не было.
    • Axios нужен был в эру Node < 18 и для удобства (interceptors, request/response transform, JSON по умолчанию).
    • Аналогия: тащить Axios в современном Node = тащить Bluebird, когда есть нативные Promise.
  • Цитата:

    «Про Axios нужно забыть. Зачем тащить лишние зависимости, когда fetch уже часть платформы?»