← JavaScript/Fetch API#88 из 383← ПредыдущийСледующий →+35 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Fetch API

Какую проблему решает Fetch

Ты разрабатываешь интернет-магазин. При добавлении товара в корзину нужно сообщить серверу. При открытии страницы — загрузить каталог. Раньше для этого использовали XMLHttpRequest — громоздкий и неудобный. Fetch API — современная замена: лаконичный, промис-based, встроен в браузер и Node.js 18+.

На основе предыдущих уроков

  • «Промисы» — fetch() возвращает Promise
  • «async/await» — с await fetch выглядит как синхронный код
  • «try/catch» — обработка сетевых ошибок
  • «JSON» — response.json() десериализует ответ
  • Базовый GET-запрос

    const response = await fetch('https://api.shop.ru/products')
    
    console.log(response.status)  // 200
    console.log(response.ok)      // true (если status 200-299)
    
    const products = await response.json()  // два await: один для запроса, один для тела
    console.log(products[0].name)

    Критически важно: fetch НЕ бросает ошибку на 4xx/5xx!

    Самая коварная особенность Fetch — HTTP-ошибки не бросают исключений:

    // Fetch не выбросит ошибку даже при 404 или 500:
    const response = await fetch('/api/nonexistent')
    console.log(response.status)  // 404
    console.log(response.ok)      // false — но исключения нет!
    
    // Нужно проверять самостоятельно:
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }

    Fetch бросает исключение только при сетевых проблемах: нет интернета, DNS не отвечает.

    POST-запрос с JSON-телом

    const response = await fetch('/api/orders', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',   // обязательно!
        'Authorization': 'Bearer ' + token,
      },
      body: JSON.stringify({
        productId: 42,
        quantity: 2,
      }),
    })
    
    if (!response.ok) throw new Error('Ошибка создания заказа')
    const order = await response.json()
    console.log('Заказ создан, id:', order.id)

    Универсальная обёртка apiClient

    В реальных проектах Fetch оборачивают в утилиту, чтобы не повторять проверки:

    async function apiRequest(url, options = {}) {
      const response = await fetch(url, {
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
        },
        ...options,
      })
    
      if (!response.ok) {
        const body = await response.json().catch(() => null)
        const message = body?.message ?? `HTTP ${response.status}`
        throw new Error(message)
      }
    
      if (response.status === 204) return null  // No Content
      return response.json()
    }
    
    // Чистое использование — без ручных проверок
    const products = await apiRequest('/api/products')
    const order = await apiRequest('/api/orders', {
      method: 'POST',
      body: JSON.stringify({ productId: 1, qty: 2 }),
    })

    AbortController — отмена запроса

    // При быстрой печати в поиске — отменяем предыдущий запрос
    let controller = null
    
    async function search(query) {
      controller?.abort()
      controller = new AbortController()
    
      try {
        const response = await fetch(`/api/search?q=${query}`, {
          signal: controller.signal,
        })
        return response.json()
      } catch (e) {
        if (e.name === 'AbortError') return null  // запрос отменён — не ошибка
        throw e
      }
    }

    Типичные ошибки

    1. Не проверили response.ok — молча обрабатывают ошибку как успех:

    // Сломано:
    const response = await fetch('/api/users/999')
    const user = await response.json()  // { message: 'Not found' }
    console.log(user.name)  // undefined — откуда ошибка?
    
    // Исправлено:
    const response = await fetch('/api/users/999')
    if (!response.ok) throw new Error('Пользователь не найден')
    const user = await response.json()

    2. Забыли второй await у response.json():

    // Сломано:
    const response = await fetch('/api/products')
    const data = response.json()   // нет await — data это Promise!
    console.log(data.length)       // undefined
    
    // Исправлено:
    const data = await response.json()

    3. Нет Content-Type для POST — сервер не понимает тело:

    // Сломано:
    await fetch('/api/orders', {
      method: 'POST',
      body: JSON.stringify(order),
    })
    
    // Исправлено:
    await fetch('/api/orders', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(order),
    })

    В реальных проектах

  • axios — популярная библиотека, автоматически проверяет статус и парсит JSON
  • React Query, SWR — кеширование, повторные запросы, состояние загрузки
  • Next.js fetch — расширенный fetch с кешированием ({ next: { revalidate: 60 } })
  • Interceptors — перехватчики для добавления токена авторизации ко всем запросам
  • Примеры

    Универсальный API-клиент для интернет-магазина с GET/POST

    // Мок fetch для sandbox (в браузере — нативный fetch с реальными URL)
    const db = {
      products: [
        { id: 1, name: 'MacBook Pro', price: 189990, stock: 5 },
        { id: 2, name: 'iPhone 15',   price: 89990,  stock: 12 },
      ],
      orders: [],
    }
    
    async function fetch(url, options = {}) {
      await new Promise(r => setTimeout(r, 40))
      const method = (options.method ?? 'GET').toUpperCase()
      const path = url.replace('https://api.shop.ru', '')
    
      if (path === '/products' && method === 'GET')
        return { ok: true, status: 200, json: async () => db.products }
    
      if (path.startsWith('/products/') && method === 'GET') {
        const id = parseInt(path.split('/')[2])
        const product = db.products.find(p => p.id === id)
        if (!product)
          return { ok: false, status: 404, json: async () => ({ message: 'Товар не найден' }) }
        return { ok: true, status: 200, json: async () => product }
      }
    
      if (path === '/orders' && method === 'POST') {
        const body = JSON.parse(options.body)
        const order = { id: db.orders.length + 1, ...body, status: 'pending' }
        db.orders.push(order)
        return { ok: true, status: 201, json: async () => order }
      }
    
      return { ok: false, status: 404, json: async () => ({ message: 'Маршрут не найден' }) }
    }
    
    // Универсальная обёртка
    async function apiRequest(url, options = {}) {
      const response = await fetch(url, {
        headers: { 'Content-Type': 'application/json', ...options.headers },
        ...options,
      })
    
      if (!response.ok) {
        const body = await response.json().catch(() => null)
        throw new Error(body?.message ?? `HTTP ${response.status}`)
      }
    
      return response.json()
    }
    
    const ShopAPI = {
      getProducts: () => apiRequest('https://api.shop.ru/products'),
      getProduct: (id) => apiRequest(`https://api.shop.ru/products/${id}`),
      createOrder: (data) => apiRequest('https://api.shop.ru/orders', {
        method: 'POST',
        body: JSON.stringify(data),
      }),
    }
    
    async function main() {
      const products = await ShopAPI.getProducts()
      console.log('Каталог:')
      products.forEach(p => console.log(`  ${p.name} — ${p.price.toLocaleString('ru-RU')} ₽`))
    
      const laptop = await ShopAPI.getProduct(1)
      console.log(`\nЗагружен: ${laptop.name}, в наличии: ${laptop.stock} шт.`)
    
      const order = await ShopAPI.createOrder({ productId: 1, quantity: 1 })
      console.log(`\nЗаказ #${order.id} создан, статус: ${order.status}`)
    
      try {
        await ShopAPI.getProduct(999)
      } catch (e) {
        console.log(`\nОшибка: ${e.message}`)
      }
    }
    
    main()

    Fetch API

    Какую проблему решает Fetch

    Ты разрабатываешь интернет-магазин. При добавлении товара в корзину нужно сообщить серверу. При открытии страницы — загрузить каталог. Раньше для этого использовали XMLHttpRequest — громоздкий и неудобный. Fetch API — современная замена: лаконичный, промис-based, встроен в браузер и Node.js 18+.

    На основе предыдущих уроков

  • «Промисы» — fetch() возвращает Promise
  • «async/await» — с await fetch выглядит как синхронный код
  • «try/catch» — обработка сетевых ошибок
  • «JSON» — response.json() десериализует ответ
  • Базовый GET-запрос

    const response = await fetch('https://api.shop.ru/products')
    
    console.log(response.status)  // 200
    console.log(response.ok)      // true (если status 200-299)
    
    const products = await response.json()  // два await: один для запроса, один для тела
    console.log(products[0].name)

    Критически важно: fetch НЕ бросает ошибку на 4xx/5xx!

    Самая коварная особенность Fetch — HTTP-ошибки не бросают исключений:

    // Fetch не выбросит ошибку даже при 404 или 500:
    const response = await fetch('/api/nonexistent')
    console.log(response.status)  // 404
    console.log(response.ok)      // false — но исключения нет!
    
    // Нужно проверять самостоятельно:
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }

    Fetch бросает исключение только при сетевых проблемах: нет интернета, DNS не отвечает.

    POST-запрос с JSON-телом

    const response = await fetch('/api/orders', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',   // обязательно!
        'Authorization': 'Bearer ' + token,
      },
      body: JSON.stringify({
        productId: 42,
        quantity: 2,
      }),
    })
    
    if (!response.ok) throw new Error('Ошибка создания заказа')
    const order = await response.json()
    console.log('Заказ создан, id:', order.id)

    Универсальная обёртка apiClient

    В реальных проектах Fetch оборачивают в утилиту, чтобы не повторять проверки:

    async function apiRequest(url, options = {}) {
      const response = await fetch(url, {
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
        },
        ...options,
      })
    
      if (!response.ok) {
        const body = await response.json().catch(() => null)
        const message = body?.message ?? `HTTP ${response.status}`
        throw new Error(message)
      }
    
      if (response.status === 204) return null  // No Content
      return response.json()
    }
    
    // Чистое использование — без ручных проверок
    const products = await apiRequest('/api/products')
    const order = await apiRequest('/api/orders', {
      method: 'POST',
      body: JSON.stringify({ productId: 1, qty: 2 }),
    })

    AbortController — отмена запроса

    // При быстрой печати в поиске — отменяем предыдущий запрос
    let controller = null
    
    async function search(query) {
      controller?.abort()
      controller = new AbortController()
    
      try {
        const response = await fetch(`/api/search?q=${query}`, {
          signal: controller.signal,
        })
        return response.json()
      } catch (e) {
        if (e.name === 'AbortError') return null  // запрос отменён — не ошибка
        throw e
      }
    }

    Типичные ошибки

    1. Не проверили response.ok — молча обрабатывают ошибку как успех:

    // Сломано:
    const response = await fetch('/api/users/999')
    const user = await response.json()  // { message: 'Not found' }
    console.log(user.name)  // undefined — откуда ошибка?
    
    // Исправлено:
    const response = await fetch('/api/users/999')
    if (!response.ok) throw new Error('Пользователь не найден')
    const user = await response.json()

    2. Забыли второй await у response.json():

    // Сломано:
    const response = await fetch('/api/products')
    const data = response.json()   // нет await — data это Promise!
    console.log(data.length)       // undefined
    
    // Исправлено:
    const data = await response.json()

    3. Нет Content-Type для POST — сервер не понимает тело:

    // Сломано:
    await fetch('/api/orders', {
      method: 'POST',
      body: JSON.stringify(order),
    })
    
    // Исправлено:
    await fetch('/api/orders', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(order),
    })

    В реальных проектах

  • axios — популярная библиотека, автоматически проверяет статус и парсит JSON
  • React Query, SWR — кеширование, повторные запросы, состояние загрузки
  • Next.js fetch — расширенный fetch с кешированием ({ next: { revalidate: 60 } })
  • Interceptors — перехватчики для добавления токена авторизации ко всем запросам
  • Примеры

    Универсальный API-клиент для интернет-магазина с GET/POST

    // Мок fetch для sandbox (в браузере — нативный fetch с реальными URL)
    const db = {
      products: [
        { id: 1, name: 'MacBook Pro', price: 189990, stock: 5 },
        { id: 2, name: 'iPhone 15',   price: 89990,  stock: 12 },
      ],
      orders: [],
    }
    
    async function fetch(url, options = {}) {
      await new Promise(r => setTimeout(r, 40))
      const method = (options.method ?? 'GET').toUpperCase()
      const path = url.replace('https://api.shop.ru', '')
    
      if (path === '/products' && method === 'GET')
        return { ok: true, status: 200, json: async () => db.products }
    
      if (path.startsWith('/products/') && method === 'GET') {
        const id = parseInt(path.split('/')[2])
        const product = db.products.find(p => p.id === id)
        if (!product)
          return { ok: false, status: 404, json: async () => ({ message: 'Товар не найден' }) }
        return { ok: true, status: 200, json: async () => product }
      }
    
      if (path === '/orders' && method === 'POST') {
        const body = JSON.parse(options.body)
        const order = { id: db.orders.length + 1, ...body, status: 'pending' }
        db.orders.push(order)
        return { ok: true, status: 201, json: async () => order }
      }
    
      return { ok: false, status: 404, json: async () => ({ message: 'Маршрут не найден' }) }
    }
    
    // Универсальная обёртка
    async function apiRequest(url, options = {}) {
      const response = await fetch(url, {
        headers: { 'Content-Type': 'application/json', ...options.headers },
        ...options,
      })
    
      if (!response.ok) {
        const body = await response.json().catch(() => null)
        throw new Error(body?.message ?? `HTTP ${response.status}`)
      }
    
      return response.json()
    }
    
    const ShopAPI = {
      getProducts: () => apiRequest('https://api.shop.ru/products'),
      getProduct: (id) => apiRequest(`https://api.shop.ru/products/${id}`),
      createOrder: (data) => apiRequest('https://api.shop.ru/orders', {
        method: 'POST',
        body: JSON.stringify(data),
      }),
    }
    
    async function main() {
      const products = await ShopAPI.getProducts()
      console.log('Каталог:')
      products.forEach(p => console.log(`  ${p.name} — ${p.price.toLocaleString('ru-RU')} ₽`))
    
      const laptop = await ShopAPI.getProduct(1)
      console.log(`\nЗагружен: ${laptop.name}, в наличии: ${laptop.stock} шт.`)
    
      const order = await ShopAPI.createOrder({ productId: 1, quantity: 1 })
      console.log(`\nЗаказ #${order.id} создан, статус: ${order.status}`)
    
      try {
        await ShopAPI.getProduct(999)
      } catch (e) {
        console.log(`\nОшибка: ${e.message}`)
      }
    }
    
    main()

    Задание

    Ты разрабатываешь клиент для API задачника (To-do list). Используй мок-функцию `fetch` (уже написана). Реализуй: 1. `getTasks()` — GET /tasks, возвращает массив задач 2. `createTask(title)` — POST /tasks с телом `{ title }`, возвращает созданную задачу 3. `deleteTask(id)` — DELETE /tasks/:id, бросает ошибку если не найдена Каждая функция должна проверять `response.ok` и бросать ошибку при неуспехе.

    Подсказка

    getTasks: throw new Error("Ошибка загрузки задач"). createTask body: JSON.stringify({ title }). deleteTask: const err = await response.json(); throw new Error(err.message). Не забудь Content-Type для POST.

    Загружаем среду выполнения...
    Загружаем AI-помощника...