Fetch API в JavaScript: простые запросы вместо сложного кода
Для кого эта статья:
- Разработчики, изучающие JavaScript и веб-разработку
- Студенты на курсах или программах по веб-разработке
Профессионалы, желающие улучшить свои навыки работы с сетевыми запросами и API
Помните, как мы все мучились с XMLHttpRequest, пытаясь отправить простой запрос к API? Тонны вложенного кода, нечитаемые обработчики событий и загадочные состояния readyState... К счастью, эти дни остались позади! Fetch API — это тот инструмент, который радикально упрощает работу с сетевыми запросами в JavaScript, делая её не только понятнее, но и мощнее. Давайте погрузимся в мир элегантных промисов и асинхронных операций, которые превращают взаимодействие с серверами из головной боли в удовольствие. 🚀
Мечтаете создавать современные веб-приложения, используя такие передовые технологии как Fetch API? В курсе Обучение веб-разработке от Skypro вы не только освоите все тонкости работы с сетевыми запросами, но и получите целостное понимание фронтенд-разработки — от основ JavaScript до продвинутых асинхронных операций. Наши студенты уже через 3 месяца уверенно строят производительные веб-интерфейсы, которые плавно взаимодействуют с любыми API.
Что такое Fetch API и почему его стоит использовать
Fetch API представляет собой современный интерфейс JavaScript для выполнения HTTP-запросов к серверу. Он был разработан как замена устаревшему XMLHttpRequest и предлагает более мощный и гибкий набор возможностей для асинхронной работы с сетью.
Ключевое преимущество Fetch API — использование промисов (Promise), что позволяет писать более чистый и понятный код при работе с асинхронными операциями. Вместо сложных конструкций с обработчиками событий и обратными вызовами, мы получаем элегантные цепочки .then() и возможность использовать async/await.
Александр Петров, ведущий фронтенд-разработчик
Когда я впервые столкнулся с Fetch API в 2016 году, это было похоже на глоток свежего воздуха. Мы работали над масштабным SPA с десятками API-эндпоинтов, и наш код с XMLHttpRequest превратился в настоящий спагетти-монстр. Переход на Fetch сократил объем кода почти на 40% и сделал его намного читаемее.
Помню, как я переписал модуль авторизации с XHR на Fetch за один вечер, сократив его со 150 строк до 70. Когда на следующий день я показал результат команде, один из джуниоров сказал: "Теперь я наконец понимаю, что происходит в этом коде!". Это было лучшим подтверждением правильности решения.
Вот основные причины, почему стоит использовать Fetch API:
- Промисы из коробки — нативная поддержка Promise API делает код более читаемым и упрощает обработку последовательных операций
- Поддержка современных стандартов — Fetch интегрирован с другими современными API, такими как Service Workers
- Упрощенный интерфейс — меньше кода для типичных операций, более понятная структура
- Стриминг данных — возможность обрабатывать ответы как потоки данных
- Легкость кэширования — простая интеграция с механизмами кэширования
| Функция | XMLHttpRequest | Fetch API |
|---|---|---|
| Асинхронная модель | Колбэки и события | Промисы и async/await |
| Обработка ошибок | Разрозненные обработчики | Унифицированный catch() |
| Прогресс загрузки | Нативная поддержка | Через Response.body |
| Поддержка в браузерах | Все браузеры | Все современные (IE требует полифил) |
| Отмена запроса | abort() | AbortController API |
Единственный существенный минус Fetch API — отсутствие поддержки в устаревших браузерах (особенно в Internet Explorer), но эта проблема легко решается с помощью полифилов для обеспечения обратной совместимости.

Основной синтаксис и JavaScript Fetch GET/POST примеры
Базовый синтаксис Fetch API удивительно прост, что делает его идеальным для ежедневного использования в веб-разработке. Начнем с самого простого примера — выполнения GET-запроса:
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Ошибка:', error));
Этот минималистичный код выполняет HTTP-запрос, преобразует ответ в JSON и выводит результат в консоль. Для сравнения, аналогичная операция с XMLHttpRequest потребовала бы в 2-3 раза больше кода! 🎯
Если вы предпочитаете современный синтаксис async/await, тот же запрос можно переписать так:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Ошибка:', error);
}
}
fetchData();
Для выполнения POST-запроса нам нужно указать дополнительные параметры, включая метод, заголовки и тело запроса:
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Алексей',
email: 'alex@example.com'
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Ошибка:', error));
Рассмотрим основные параметры, которые можно передать в конфигурационный объект fetch:
- method — HTTP-метод (GET, POST, PUT, DELETE и др.)
- headers — объект с HTTP-заголовками
- body — тело запроса (строка, FormData, Blob и др.)
- mode — режим запроса (cors, no-cors, same-origin)
- credentials — включение кук (omit, same-origin, include)
- cache — стратегия кэширования (default, no-store, reload и др.)
- redirect — обработка редиректов (follow, error, manual)
- referrer — политика referrer
- signal — для отмены запроса (AbortController)
Вот полезные примеры для различных сценариев использования:
// PUT-запрос для обновления данных
fetch('https://api.example.com/users/1', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Новое имя'
})
})
.then(response => response.json())
.then(data => console.log('Обновлено:', data));
// DELETE-запрос
fetch('https://api.example.com/users/1', {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
console.log('Пользователь удален');
}
});
// Отправка формы
const formData = new FormData();
formData.append('username', 'john_doe');
formData.append('avatar', fileInput.files[0]);
fetch('https://api.example.com/profile', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => console.log('Профиль обновлен:', data));
Работа с заголовками и форматами данных в Fetch API
Гибкость Fetch API проявляется в полной мере при работе с HTTP-заголовками и различными форматами данных. Правильное использование заголовков критически важно для эффективного взаимодействия с API и обеспечения безопасности ваших запросов. 🔐
Для управления заголовками Fetch API предоставляет специальный интерфейс Headers, который можно использовать двумя способами:
// Способ 1: через объект в fetch()
fetch('https://api.example.com/data', {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer TOKEN123',
'X-Custom-Header': 'CustomValue'
}
});
// Способ 2: с использованием объекта Headers
const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Authorization', 'Bearer TOKEN123');
fetch('https://api.example.com/data', { headers });
Объект Headers предоставляет удобные методы для манипуляции заголовками:
- append() — добавляет новый заголовок
- set() — устанавливает или заменяет заголовок
- get() — получает значение заголовка
- has() — проверяет наличие заголовка
- delete() — удаляет заголовок
- forEach() — перебирает все заголовки
При работе с API вам придётся иметь дело с различными форматами данных. Fetch API предоставляет несколько встроенных методов для обработки ответов:
// Работа с JSON
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));
// Получение текстового ответа
fetch('https://api.example.com/text')
.then(response => response.text())
.then(text => console.log(text));
// Получение бинарных данных
fetch('https://api.example.com/image.jpg')
.then(response => response.blob())
.then(blob => {
const imgUrl = URL.createObjectURL(blob);
const imgElement = document.getElementById('myImage');
imgElement.src = imgUrl;
});
// Получение формата formData
fetch('https://api.example.com/form')
.then(response => response.formData())
.then(formData => {
console.log(formData.get('fieldName'));
});
// Работа с потоковыми данными
fetch('https://api.example.com/stream')
.then(response => {
const reader = response.body.getReader();
function processStream({ done, value }) {
if (done) return;
// Обработка порции данных (value – это Uint8Array)
console.log('Получена часть данных:', value);
// Запрашиваем следующую порцию
return reader.read().then(processStream);
}
return reader.read().then(processStream);
});
Особо стоит отметить работу с различными типами контента при отправке данных:
| Тип контента | Content-Type | Формирование body | Пример использования |
|---|---|---|---|
| JSON | application/json | JSON.stringify(obj) | Большинство современных API |
| Формы | multipart/form-data | new FormData() | Загрузка файлов |
| URL-encoded | application/x-www-form-urlencoded | new URLSearchParams() | Традиционные формы |
| Текст | text/plain | 'строка текста' | Простые текстовые запросы |
| XML | application/xml | строка XML | SOAP и устаревшие API |
| Бинарные | application/octet-stream | Blob или ArrayBuffer | Отправка файлов напрямую |
Вот пример отправки данных в формате URL-encoded, который часто используется для совместимости со старыми API:
const params = new URLSearchParams();
params.append('username', 'john_doe');
params.append('password', 'secret123');
fetch('https://api.example.com/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params
})
.then(response => response.json())
.then(data => console.log('Успешная авторизация:', data));
Михаил Соколов, тимлид JavaScript-разработки
Однажды наша команда столкнулась с серьезной проблемой при интеграции с API партнера. Их сервер требовал специфических заголовков и формата данных, которые нам было непросто реализовать через XMLHttpRequest.
После долгих мучений я предложил перейти на Fetch API. Ключевым моментом стала гибкость при работе с заголовками. Мы столкнулись с API, которое использовало нестандартную аутентификацию, где токен нужно было передавать в специальном заголовке X-Custom-Auth вместе с хешем от времени запроса.
Fetch сделал эту задачу тривиальной. Мы создавали
Headersобъект, заполняли все необходимые поля включая динамически вычисляемый хеш, и всё работало как часы. Более того, проект требовал загрузки бинарных данных, где streaming API из Fetch позволил нам реализовать индикатор прогресса при минимальных усилиях.
Когда интеграция была завершена, мы сократили около 400 строк запутанного XHR-кода до примерно 150 строк чистого и понятного кода на Fetch. Это был момент, когда даже убежденные консерваторы в команде признали преимущества современного API.
Обработка ошибок и управление таймаутами в Fetch API
Обработка ошибок — ключевой аспект работы с любым сетевым API. Fetch немного отличается от привычного XMLHttpRequest тем, что промис не отклоняется при ответах с HTTP-ошибками (например, 404 или 500). Вместо этого он считается успешно выполненным, но со свойством ok, установленным в false. 🛠
Это важное отличие требует явной проверки статуса ответа:
fetch('https://api.example.com/nonexistent')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => console.log('Данные:', data))
.catch(error => console.error('Проблема с fetch:', error));
Для более детального анализа ошибок, полезно изучить доступные свойства объекта Response:
- status — HTTP-статус ответа (например, 200, 404, 500)
- statusText — текстовое описание статуса
- ok — булево значение, true если статус в диапазоне 200-299
- headers — заголовки ответа
- redirected — был ли ответ получен после редиректа
- type — тип ответа (basic, cors, error, opaque)
- url — финальный URL после всех редиректов
Вот более комплексный пример обработки ошибок с учетом различных сценариев:
async function fetchWithErrorHandling(url) {
try {
const response = await fetch(url);
// Проверка статуса HTTP
if (!response.ok) {
// Пытаемся получить подробности ошибки из JSON
try {
const errorData = await response.json();
throw new Error(`API error: ${errorData.message || response.statusText}`);
} catch (jsonError) {
// Если не удалось распарсить JSON, используем стандартное сообщение
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
}
}
return await response.json();
} catch (error) {
// Обработка сетевых ошибок и прочих проблем
if (error.name === 'AbortError') {
console.log('Запрос был отменен');
} else if (error instanceof TypeError) {
console.error('Сетевая ошибка или CORS-проблема:', error);
} else {
console.error('Ошибка в процессе запроса:', error);
}
throw error; // Перебрасываем дальше для обработки выше
}
}
// Использование
fetchWithErrorHandling('https://api.example.com/data')
.then(data => console.log('Успех:', data))
.catch(error => console.error('Итоговая обработка ошибки:', error));
К сожалению, Fetch API не имеет встроенной поддержки таймаутов, но мы можем реализовать эту функциональность, комбинируя Fetch с Promise.race():
function fetchWithTimeout(url, options = {}, timeout = 5000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout exceeded')), timeout)
)
]);
}
fetchWithTimeout('https://api.example.com/data', {}, 3000)
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error with fetch:', error));
Для отмены запросов (что особенно полезно при переходах между страницами или при обновлении поиска) используется интерфейс AbortController:
// Создаем контроллер
const controller = new AbortController();
const signal = controller.signal;
// Запускаем запрос с передачей signal
fetch('https://api.example.com/large-data', { signal })
.then(response => response.json())
.then(data => console.log('Данные получены:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Запрос был отменен пользователем');
} else {
console.error('Другая ошибка:', error);
}
});
// Где-то в другом месте кода (например, при клике на кнопку "Отмена")
controller.abort();
Комбинируя эти техники, мы можем создать универсальную функцию для работы с API, которая поддерживает таймауты и отмену запросов:
function enhancedFetch(url, options = {}) {
const { timeout = 10000, ...fetchOptions } = options;
// Создаем контроллер для отмены запроса
const controller = new AbortController();
const { signal } = controller;
// Объединяем наш сигнал с тем, который мог быть передан в options
const combinedOptions = {
...fetchOptions,
signal: fetchOptions.signal
? composeAbortSignals(signal, fetchOptions.signal)
: signal
};
// Создаем таймер для таймаута
const timeoutId = setTimeout(() => controller.abort(), timeout);
// Выполняем запрос
return fetch(url, combinedOptions)
.then(response => {
clearTimeout(timeoutId);
return response;
})
.catch(error => {
clearTimeout(timeoutId);
if (error.name === 'AbortError' && !fetchOptions.signal?.aborted) {
throw new Error(`Request timeout after ${timeout}ms`);
}
throw error;
});
}
// Вспомогательная функция для объединения нескольких сигналов
function composeAbortSignals(signal1, signal2) {
const controller = new AbortController();
// Отменяем новый контроллер, если любой из входных сигналов сработает
[signal1, signal2].forEach(signal => {
if (signal.aborted) {
controller.abort();
return;
}
signal.addEventListener('abort', () => controller.abort());
});
return controller.signal;
}
Переход с XMLHttpRequest на Fetch API: практический опыт
Переход с XMLHttpRequest на Fetch API — одно из самых выгодных обновлений, которые вы можете сделать для своей кодовой базы. Этот процесс не только упрощает код, но и делает его более поддерживаемым и расширяемым. 🔄
Давайте рассмотрим практические шаги и паттерны для плавной миграции:
- Пошаговый подход — не пытайтесь переписать все XHR-вызовы одновременно
- Создание обертки — разработайте унифицированный интерфейс поверх Fetch API
- Тщательное тестирование — каждый переписанный запрос должен проходить те же тесты
- Обеспечение обратной совместимости — используйте полифилы для старых браузеров
- Пересмотр логики обработки ошибок — учитывайте разницу в подходе к ошибкам
Вот сравнение кода для типичного запроса в обоих вариантах:
// XMLHttpRequest версия
function getDataWithXHR(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const data = JSON.parse(xhr.responseText);
callback(null, data);
} catch (e) {
callback(new Error('Ошибка парсинга JSON'));
}
} else {
callback(new Error(`HTTP ошибка! Статус: ${xhr.status}`));
}
};
xhr.onerror = function() {
callback(new Error('Сетевая ошибка'));
};
xhr.timeout = 5000;
xhr.ontimeout = function() {
callback(new Error('Таймаут запроса'));
};
xhr.send();
}
// Эквивалент на Fetch API
function getDataWithFetch(url) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => reject(new Error('Таймаут запроса')), 5000);
fetch(url)
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ошибка! Статус: ${response.status}`);
}
return response.json();
})
.then(data => resolve(data))
.catch(error => {
clearTimeout(timeoutId);
reject(error);
});
});
}
// Современный вариант с async/await
async function getDataWithFetchModern(url) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ошибка! Статус: ${response.status}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Таймаут запроса');
}
throw error;
}
}
Для упрощения миграции можно создать универсальную функцию-обертку, которая инкапсулирует все типичные операции:
// Универсальный клиент для API на основе Fetch
class ApiClient {
constructor(baseUrl = '', defaultOptions = {}) {
this.baseUrl = baseUrl;
this.defaultOptions = defaultOptions;
}
async request(endpoint, options = {}) {
const url = this.baseUrl + endpoint;
const fetchOptions = { ...this.defaultOptions, ...options };
// Добавляем обработку таймаутов
const { timeout = 10000 } = fetchOptions;
const controller = new AbortController();
const { signal } = controller;
if (fetchOptions.signal) {
// Если уже есть signal, создаем композитный
fetchOptions.signal.addEventListener('abort', () => controller.abort());
}
fetchOptions.signal = signal;
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, fetchOptions);
clearTimeout(timeoutId);
// Стандартная обработка ошибок
if (!response.ok) {
let errorMessage;
try {
const errorData = await response.json();
errorMessage = errorData.message || response.statusText;
} catch {
errorMessage = response.statusText;
}
throw new Error(`HTTP ошибка! Статус: ${response.status}, Сообщение: ${errorMessage}`);
}
// Определяем тип ответа
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return await response.json();
} else if (contentType.includes('text/')) {
return await response.text();
} else {
return await response.blob();
}
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Запрос отменен: ${error.message || 'превышен таймаут'}`);
}
throw error;
}
}
// Удобные методы для разных HTTP-методов
get(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'GET' });
}
post(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options.headers
},
body: JSON.stringify(data)
});
}
put(endpoint, data, options = {}) {
return this.request(endpoint, {
...options,
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...options.headers
},
body: JSON.stringify(data)
});
}
delete(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'DELETE' });
}
}
// Использование
const api = new ApiClient('https://api.example.com');
// GET-запрос
api.get('/users')
.then(users => console.log(users))
.catch(error => console.error(error));
// POST-запрос
api.post('/users', { name: 'Иван', email: 'ivan@example.com' })
.then(newUser => console.log(newUser))
.catch(error => console.error(error));
Путешествие от XMLHttpRequest к Fetch API похоже на пересадку с велосипеда на электросамокат — сначала немного непривычно, но очень быстро понимаешь преимущества. Промисы и асинхронные функции превращают сложные цепочки callback-ов в линейный, читаемый код. Вместо проверки различных состояний readyState и ловушек onreadystatechange, вы получаете элегантные цепочки
then()или строкиawait. Добавьте к этому поддержку потоков данных, удобную работу с заголовками и простое управление кэшированием — и вы получите API, который делает сетевые запросы радостью, а не испытанием. Переходите на Fetch сегодня, и ваши будущие коллеги (включая будущего вас) скажут спасибо! 🚀