Fetch API
Fetch API — современный интерфейс для выполнения HTTP-запросов, возвращающий Promise.
Зачем нужно
Fetch — стандартный способ общения с сервером из браузера. Заменил XMLHttpRequest. Используется для загрузки данных, отправки форм, работы с REST API.
Где используется
- AJAX-запросы
- REST API (CRUD-операции)
- Загрузка/отправка файлов
- Работа с GraphQL
- Server-Sent Events
Предпосылки
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
});
Практика
- Реализуй CRUD для сущности "задача" (todo)
- Загрузи данные из 3 разных API параллельно
- Реализуй retry с экспоненциальной задержкой
- Добавь таймаут 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 устарел. Node 18+ имеет нативный
- Цитата:
«Про Axios нужно забыть. Зачем тащить лишние зависимости, когда fetch уже часть платформы?»