Перейти к содержанию
Educora
Продвинутый24 мин24 / 27

fetch и REST API

Общайся с серверами: как работает HTTP, ресурсы и методы REST, `fetch` с JSON, коды статуса, обработка ошибок, тайм-ауты через `AbortController` и параллельные запросы.

Проверь себя
В этом уроке ты узнаешь
  • Объяснять HTTP-запрос и ответ: метод, URL, заголовки, тело, код статуса
  • Отправлять GET- и POST-запросы через fetch, читать JSON и правильно обрабатывать ошибки HTTP и сети
  • Строить надёжные асинхронные приёмы: тайм-ауты, повторные попытки, параллельные запросы и URLSearchParams

Прогноз погоды в приложении, курсы валют, лента в соцсети — всё это приходит с серверов через API. Твой код отправляет HTTP-запрос, а сервер отвечает данными, обычно в формате JSON. fetch — современный встроенный инструмент для этого в браузерах и в Node.js (начиная с версии 18).

HTTP за одну минуту

Запрос состоит из метода, URL, заголовков (headers) и иногда тела (body). В ответе есть код статуса, заголовки и тело. Первая цифра кода статуса задаёт смысл: 2xx — успех, 3xx — перенаправление, 4xx — ошибка клиента (неверный запрос), 5xx — ошибка сервера.

Text
POST /api/orders HTTP/1.1
Host: shop.example.com
Content-Type: application/json
Authorization: Bearer <token>

{"productId": 7, "quantity": 2}

HTTP/1.1 201 Created
Content-Type: application/json

{"id": 1043, "productId": 7, "quantity": 2, "status": "new"}
Сверху — запрос, снизу — ответ сервера. Заголовки от тела отделяет пустая строка.
Определение
REST

Стиль построения API: данные — это ресурсы со своими URL (/users, /users/42), а действия выражаются методами HTTP. Каждый запрос независим (сервер «не помнит» предыдущий), а данные обычно передаются в формате JSON.

МетодСмыслПример и успешный ответ
GETпрочитатьGET /users/42 → 200 OK
POSTсоздатьPOST /orders → 201 Created
PUT / PATCHзаменить / частично обновитьPATCH /users/42 → 200 OK
DELETEудалитьDELETE /orders/1043 → 204 No Content
Коды ошибок, которые встречаются часто: 400 (неверные данные), 401 (нужен вход), 403 (нет прав), 404 (не найдено), 429 (слишком много запросов), 500 и 503 (проблемы сервера).

GET через fetch

fetch(url) возвращает промис, который выполняется с объектом Response. Работа идёт в два шага: сначала ждёшь ответ (приходят заголовки), затем читаешь тело через response.json() — это тоже промис. Главное: fetch отклоняется только при сетевой ошибке (нет интернета, неизвестный домен, CORS). Ответы 404 и 500 тоже «успешно приходят», поэтому всегда проверяй response.ok (статус 200–299).

JavaScript
async function getUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json();
}

try {
  const user = await getUser(42);
  console.log(user.name, user.city);
  await getUser(999999);
} catch (error) {
  console.log('Request failed:', error.message);
}
Ожидаемый результат
Aysel Baku
Request failed: HTTP 404
api.example.com — условный адрес: у настоящего API поля будут называться иначе. Запускатель на этой странице не отправляет настоящих запросов, поэтому ниже мы сымитируем сервер.

Объект Response можно создать прямо в JavaScript. Это позволяет сымитировать сервер и тренироваться без интернета: fakeFetch возвращает Response, как настоящий fetch, а твой код не замечает разницы. В тестах поступают именно так.

JavaScript
const db = { 42: { id: 42, name: 'Aysel', city: 'Baku' } };

async function fakeFetch(url) {
  await new Promise((resolve) => setTimeout(resolve, 50));
  const id = Number(url.split('/').pop());
  const user = db[id];
  return new Response(JSON.stringify(user ?? { error: 'Not found' }), {
    status: user ? 200 : 404,
    headers: { 'Content-Type': 'application/json' },
  });
}

for (const id of [42, 7]) {
  const response = await fakeFetch(`/api/users/${id}`);
  console.log(response.status, response.ok, response.headers.get('content-type'));
  console.log(await response.json());
}
▸ Ожидаемый результат
200 true application/json
{ id: 42, name: 'Aysel', city: 'Baku' }
404 false application/json
{ error: 'Not found' }

POST, заголовки и ошибки

Чтобы отправить данные, передай в fetch вторым аргументом объект настроек: method, headers и body. Тело должно быть текстом, поэтому объект превращают в строку через JSON.stringify, а заголовок Content-Type: application/json сообщает серверу формат. API, требующие входа, обычно ждут заголовок Authorization: Bearer <token>.

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

  if (response.status === 401) throw new Error('Please log in again');
  if (!response.ok) throw new Error(`Server error ${response.status}`);
  return response.json();
}

const saved = await createOrder({ productId: 7, quantity: 2 }, token);
console.log('Created order', saved.id, saved.status);
Ожидаемый результат
Created order 1043 new

Надёжные асинхронные приёмы

Настоящая сеть медленная и ненадёжная. Помогают четыре привычки: собирай параметры запроса через URL и URLSearchParams (спецсимволы кодируются автоматически); отправляй независимые запросы параллельно через Promise.all; ставь ограничение по времени — fetch(url, { signal: AbortSignal.timeout(8000) }) отменит запрос через 8 секунд с ошибкой TimeoutError; при временных сбоях (5xx, 429) повторяй попытку с растущей паузой.

JavaScript
const url = new URL('https://api.example.com/search');
url.searchParams.set('q', 'plov & dolma');
url.searchParams.set('city', 'Baku');
url.searchParams.set('page', 2);

console.log(url.toString());
console.log(url.searchParams.get('q'));
console.log(Object.fromEntries(url.searchParams));
▸ Ожидаемый результат
https://api.example.com/search?q=plov+%26+dolma&city=Baku&page=2
plov & dolma
{ q: 'plov & dolma', city: 'Baku', page: '2' }
Склеенный вручную ?q=plov & dolma сломал бы запрос: & был бы принят за новый параметр. URLSearchParams кодирует его как %26.
JavaScript
function slowServer(ms, signal) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => resolve('data'), ms);
    signal.addEventListener('abort', () => {
      clearTimeout(timer);
      reject(new DOMException('Request took too long', 'TimeoutError'));
    });
  });
}

async function withTimeout(ms, limit) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), limit);
  try {
    return await slowServer(ms, controller.signal);
  } catch (error) {
    return `failed: ${error.name}`;
  } finally {
    clearTimeout(timer);
  }
}

console.log(await withTimeout(50, 200));
console.log(await withTimeout(500, 200));
▸ Ожидаемый результат
data
failed: TimeoutError
AbortController создаёт сигнал отмены; настоящий fetch принимает этот signal. AbortSignal.timeout(ms) делает то же самое в одну строку.
Задание

Напиши помощника getJSON(url): он вызывает fakeFetch(url), при неуспешном ответе выбрасывает ошибку с сообщением HTTP <status>, а иначе возвращает разобранный JSON.

Задание · JavaScript
const products = { 1: { id: 1, title: 'Notebook', price: 5 } };

async function fakeFetch(url) {
  const id = Number(url.split('/').pop());
  const product = products[id];
  return new Response(JSON.stringify(product ?? { error: 'Not found' }), {
    status: product ? 200 : 404,
    headers: { 'Content-Type': 'application/json' },
  });
}

async function getJSON(url) {
  // call fakeFetch, check response.ok, return the JSON
}

for (const url of ['/api/products/1', '/api/products/99']) {
  try {
    console.log(await getJSON(url));
  } catch (error) {
    console.log('Error:', error.message);
  }
}
▸ Ожидаемый результат
{ id: 1, title: 'Notebook', price: 5 }
Error: HTTP 404
Задание

flakyFetch первые два раза возвращает 503. Напиши fetchWithRetry(url, retries): после каждой неудачной попытки она выводит Attempt N: 503, при ошибке 5xx ждёт 100, затем 200 мс (каждый раз вдвое дольше) и пробует снова, а когда повторы retries закончились или пришёл 4xx — выбрасывает HTTP <status>.

Задание · JavaScript
let calls = 0;
async function flakyFetch(url) {
  calls++;
  const ok = calls >= 3;
  return new Response(JSON.stringify(ok ? { rates: { USD: 1.7 } } : {}), { status: ok ? 200 : 503 });
}

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function fetchWithRetry(url, retries) {
  // only one attempt for now: add retries with a growing delay
  const response = await flakyFetch(url);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

const data = await fetchWithRetry('/api/rates', 3);
console.log('Rates:', data.rates);
▸ Ожидаемый результат
Attempt 1: 503
Attempt 2: 503
Rates: { USD: 1.7 }

Главное

  • HTTP: запрос = метод + URL + заголовки + тело; ответ = статус + заголовки + тело. 2xx — успех, 4xx — ошибка клиента, 5xx — ошибка сервера.
  • REST: ресурсы живут по URL, а действия выражаются методами: GET читает, POST создаёт, PUT/PATCH обновляют, DELETE удаляет.
  • fetch отклоняется только при сетевых сбоях — всегда проверяй response.ok и читай тело один раз через await response.json().
  • Для POST передавай method, заголовок Content-Type: application/json и body: JSON.stringify(data).
  • Надёжность: ограничение по времени (AbortSignal.timeout), повторы при временных ошибках 5xx, Promise.all для параллельных запросов и URLSearchParams для параметров.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
fetch отправлен на адрес, который возвращает 404. Что произойдёт?