📘 TypeScript

Working with Network and HTTP API in TypeScript: A Detailed Guide

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты сидишь в уютном кафе. Ты — это твоя программа (клиент). На стойке стоит официант — это HTTP-сервер. А за стойкой, на кухне, трудятся повара — это база данных или другое приложение, которое хранит нужную информацию.

Ты хочешь заказать пиццу. Ты не идёшь на кухню сам (это было бы небезопасно и неудобно). Вместо этого ты подзываешь официанта и говоришь: «Принесите, пожалуйста, пиццу «Маргарита»». Это и есть HTTP-запрос. Официант идёт на кухню, передаёт заказ повару, забирает готовую пиццу и приносит её тебе. Это HTTP-ответ.

В мире программирования всё работает точно так же. Твоя программа (на TypeScript) хочет получить данные: прогноз погоды, список пользователей, курс валют. Она не лезет напрямую в чужую базу данных. Вместо этого она отправляет HTTP-запрос на специальный адрес (URL) — это как сказать официанту «Принеси меню». Сервер обрабатывает запрос и возвращает ответ (обычно в формате JSON — это как поднос с едой).

Работа с сетью — это умение твоей программы общаться с другими программами через интернет, используя понятный всем язык HTTP.

Зачем это нужно? Без этого невозможно представить ни одно современное приложение:

  • Мобильное приложение погоды — каждые 10 минут отправляет запрос на сервер и получает свежие данные.
  • Интернет-магазин — когда ты нажимаешь «Купить», твой браузер отправляет запрос, чтобы добавить товар в корзину.
  • Чат — отправка сообщения — это тоже HTTP-запрос.
  • Авторизация — когда ты вводишь логин и пароль, программа отправляет их на сервер для проверки.

В этой статье мы научимся писать такие запросы на TypeScript — языке, который делает JavaScript строгим и надёжным.

Теория: как это работает изнутри

Давай заглянем «под капот» и разберёмся, из чего состоит любое сетевое взаимодействие.

HTTP — язык общения

HTTP (HyperText Transfer Protocol) — это набор правил, по которым клиент и сервер общаются. Представь, что это грамматика языка. Если ты скажешь официанту «Пицца хочу», он, возможно, поймёт, но это невежливо и неправильно. В HTTP тоже есть строгие правила.

Основные понятия

Термин Что это? Аналогия
URL Адрес ресурса в интернете Адрес кафе, куда ты идёшь
Метод HTTP Действие, которое ты хочешь выполнить «Принеси», «Унеси», «Замени»
Заголовки (Headers) Дополнительная информация о запросе «Я хочу пиццу без лука» (пожелания)
Тело (Body) Данные, которые ты отправляешь Список ингредиентов для пиццы
Статус ответа Код, который говорит, как прошёл запрос «Всё готово» (200) или «Такого нет в меню» (404)

Методы HTTP — глаголы нашего языка

Самые популярные методы, которые ты будешь использовать постоянно:

  • GET — получить данные. «Покажи меню». Не меняет ничего на сервере.
  • POST — создать что-то новое. «Добавь новую пиццу в меню».
  • PUT — полностью заменить существующее. «Замени пиццу «Маргарита» на «Пепперони»».
  • PATCH — частично изменить. «Убери оливки из пиццы «Маргарита»».
  • DELETE — удалить. «Убери пиццу «Маргарита» из меню».

Коды ответа — как прошёл запрос?

Сервер всегда отвечает числом. По этому числу ты сразу понимаешь, всё ли хорошо:

Код Значение Пример из жизни
200 OK — всё отлично Официант принёс пиццу
201 Created — создано «Мы добавили вашу пиццу в меню»
400 Bad Request — ты ошибся «Вы заказали пиццу, которой нет в меню»
401 Unauthorized — нужен пропуск «Сначала покажите членский билет»
404 Not Found — не найдено «Такой пиццы у нас нет»
500 Internal Server Error — проблемы на кухне «У повара сломалась печь»

JSON — язык данных

Когда сервер возвращает данные, он обычно использует JSON (JavaScript Object Notation). Это просто текст, который выглядит как объект JavaScript. Например:

{
  "name": "Маргарита",
  "price": 450,
  "ingredients": ["сыр", "томаты", "базилик"]
}

TypeScript умеет превращать такой текст в настоящие объекты и обратно.

Синтаксис в TypeScript: все формы и варианты

В TypeScript есть два основных способа работать с сетью: старый добрый XMLHttpRequest (почти не используется) и современный fetch. Мы будем изучать fetch — он проще, мощнее и поддерживается везде.

Базовый синтаксис fetch

// Самый простой GET-запрос
fetch('https://api.example.com/users')
  .then(ответ => ответ.json())  // Превращаем ответ в объект
  .then(данные => console.log(данные)); // Работаем с данными

Но в современном TypeScript мы чаще используем async/await — это делает код похожим на обычные последовательные действия:

// Функция, которая умеет ждать
async function получитьПользователей() {
  // await — значит "жди, пока придёт ответ"
  const ответ = await fetch('https://api.example.com/users');
  const данные = await ответ.json();
  console.log(данные);
}

Полный синтаксис с настройками

async function отправитьЗапрос() {
  const ответ = await fetch('https://api.example.com/users', {
    // Какой метод используем
    method: 'POST',
    // Заголовки — дополнительная информация
    headers: {
      'Content-Type': 'application/json', // Говорим, что отправляем JSON
      'Authorization': 'Bearer токен_доступа' // Если нужен пропуск
    },
    // Тело запроса — то, что отправляем
    body: JSON.stringify({
      name: 'Иван',
      age: 25
    })
  });

  // Проверяем, всё ли хорошо
  if (!ответ.ok) {
    throw new Error(`Ошибка HTTP: ${ответ.status}`);
  }

  const результат = await ответ.json();
  return результат;
}

Типизация ответов — суперсила TypeScript

В TypeScript мы можем указать, какой формы будут данные, которые вернёт сервер. Это помогает не ошибиться:

// Создаём интерфейс — описание того, что мы ждём
interface Пользователь {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

async function получитьПользователя(id: number): Promise<Пользователь> {
  const ответ = await fetch(`https://api.example.com/users/${id}`);
  const пользователь: Пользователь = await ответ.json();
  return пользователь;
}

// Теперь TypeScript знает, что у пользователя есть поля id, name, email, isActive
const пользователь = await получитьПользователя(1);
console.log(пользователь.name); // TypeScript не даст написать пользователь.имя — ошибка!

Примеры кода — от простого к сложному

Пример 1: Простой GET-запрос к тестовому API

// Используем бесплатное тестовое API jsonplaceholder
async function получитьЗадачи() {
  // Отправляем GET-запрос
  const ответ = await fetch('https://jsonplaceholder.typicode.com/todos');
  
  // Превращаем ответ в массив объектов
  const задачи = await ответ.json();
  
  // Выводим первые 3 задачи
  console.log('Первые 3 задачи:');
  for (let i = 0; i < 3; i++) {
    console.log(`Задача ${i + 1}: ${задачи[i].title}`);
  }
}

// Запускаем функцию
получитьЗадачи();

Объяснение: Мы просто просим сервер дать нам список задач. Сервер возвращает массив из 200 задач. Мы берём первые три и выводим их названия.

Пример 2: POST-запрос — создаём новый ресурс

interface НоваяЗадача {
  title: string;
  completed: boolean;
  userId: number;
}

async function создатьЗадачу() {
  // Готовим данные для отправки
  const новаяЗадача: НоваяЗадача = {
    title: 'Выучить TypeScript',
    completed: false,
    userId: 1
  };

  const ответ = await fetch('https://jsonplaceholder.typicode.com/todos', {
    method: 'POST', // Говорим, что хотим создать
    headers: {
      'Content-Type': 'application/json' // Отправляем JSON
    },
    body: JSON.stringify(новаяЗадача) // Превращаем объект в строку JSON
  });

  const созданнаяЗадача = await ответ.json();
  console.log('Создана задача с ID:', созданнаяЗадача.id);
}

создатьЗадачу();

Объяснение: Мы отправляем на сервер новую задачу. Сервер принимает её, присваивает ID и возвращает нам готовый объект. Обрати внимание: мы используем JSON.stringify(), чтобы превратить объект в строку — по сети можно передавать только текст.

Пример 3: Обработка ошибок с try/catch

async function безопасныйЗапрос() {
  try {
    // Пробуем выполнить запрос
    const ответ = await fetch('https://jsonplaceholder.typicode.com/todos/99999');
    
    // Проверяем статус ответа
    if (!ответ.ok) {
      throw new Error(`Сервер вернул ошибку: ${ответ.status} ${ответ.statusText}`);
    }
    
    const данные = await ответ.json();
    console.log('Успех!', данные);
    
  } catch (ошибка) {
    // Если что-то пошло не так — ловим ошибку
    console.error('Произошла ошибка:', ошибка.message);
    
    // Можно показать пользователю понятное сообщение
    alert('Не удалось загрузить данные. Проверьте интернет-соединение.');
  }
}

безопасныйЗапрос();

Объяснение: Сеть — штука ненадёжная. Сервер может быть недоступен, интернет может пропасть. try/catch — это как страховочная сетка. Если что-то пошло не так, программа не упадёт, а gracefully (красиво) обработает ошибку.

Пример 4: Отправка данных формы

async function отправитьФорму() {
  // Создаём объект FormData (как форма на сайте)
  const форма = new FormData();
  форма.append('username', 'ivan_petrov');
  форма.append('password', 'мой_секретный_пароль');
  форма.append('remember', 'true');

  const ответ = await fetch('https://api.example.com/login', {
    method: 'POST',
    // Не ставим Content-Type! FormData сам всё настроит
    body: форма
  });

  if (ответ.ok) {
    console.log('Вход выполнен успешно!');
  } else {
    console.log('Ошибка входа. Проверьте логин и пароль.');
  }
}

Объяснение: Иногда нужно отправить данные не в JSON, а как обычную форму. Для этого есть FormData. Он сам добавляет правильные заголовки.

Пример 5: Загрузка файла на сервер

async function загрузитьФайл(файл: File) {
  // Создаём форму и добавляем файл
  const форма = new FormData();
  форма.append('file', файл); // Файл из input type="file"

  const ответ = await fetch('https://api.example.com/upload', {
    method: 'POST',
    body: форма
  });

  if (ответ.ok) {
    const результат = await ответ.json();
    console.log('Файл загружен! URL:', результат.url);
  } else {
    console.error('Ошибка загрузки файла');
  }
}

// Пример использования (в браузере):
// const input = document.querySelector('input[type="file"]');
// input.addEventListener('change', () => {
//   загрузитьФайл(input.files[0]);
// });

Объяснение: Загрузка файлов — это тоже POST-запрос, только в теле мы передаём не текст, а бинарные данные файла. FormData умеет с этим работать.

Пример 6: Работа с заголовками и токеном авторизации

interface ДанныеПрофиля {
  id: number;
  name: string;
  email: string;
}

async function получитьПрофиль(токен: string): Promise<ДанныеПрофиля> {
  const ответ = await fetch('https://api.example.com/profile', {
    method: 'GET',
    headers: {
      // Стандартный способ передать токен
      'Authorization': `Bearer ${токен}`,
      // Говорим, что хотим получить JSON
      'Accept': 'application/json'
    }
  });

  if (!ответ.ok) {
    if (ответ.status === 401) {
      throw new Error('Токен истёк. Нужно войти заново.');
    }
    throw new Error(`Ошибка: ${ответ.status}`);
  }

  const профиль: ДанныеПрофиля = await ответ.json();
  return профиль;
}

// Использование:
// const токен = localStorage.getItem('auth_token');
// const профиль = await получитьПрофиль(токен);

Объяснение: Многие API требуют авторизации. Токен — это как электронный пропуск. Мы передаём его в заголовке Authorization. Если токен неправильный или истёк, сервер вернёт 401.

Пример 7: Параллельные запросы (Promise.all)

async function загрузитьДашборд() {
  try {
    // Запускаем несколько запросов одновременно
    const [пользователи, задачи, посты] = await Promise.all([
      fetch('https://jsonplaceholder.typicode.com/users').then(r => r.json()),
      fetch('https://jsonplaceholder.typicode.com/todos').then(r => r.json()),
      fetch('https://jsonplaceholder.typicode.com/posts').then(r => r.json())
    ]);

    console.log(`Загружено ${пользователи.length} пользователей`);
    console.log(`Загружено ${задачи.length} задач`);
    console.log(`Загружено ${посты.length} постов`);

    // Теперь у нас есть все данные для отображения дашборда
    return { пользователи, задачи, посты };
    
  } catch (ошибка) {
    console.error('Не удалось загрузить дашборд:', ошибка);
    throw ошибка;
  }
}

// Использование:
// const дашборд = await загрузитьДашборд();

Объяснение: Если нужно загрузить несколько ресурсов, не обязательно ждать каждый по очереди. Promise.all запускает все запросы одновременно и ждёт, пока завершатся все. Это сильно ускоряет работу.

Пример 8: Отмена запроса (AbortController)

function создатьЗапросСТаймаутом() {
  // Создаём контроллер для отмены
  const контроллер = new AbortController();
  
  // Устанавливаем таймаут — 5 секунд
  const таймаут = setTimeout(() => {
    контроллер.abort(); // Отменяем запрос
    console.log('Запрос отменён по таймауту');
  }, 5000);

  return fetch('https://api.example.com/медленный_запрос', {
    signal: контроллер.signal // Передаём сигнал
  }).finally(() => {
    clearTimeout(таймаут); // Очищаем таймаут, если запрос выполнился
  });
}

// Использование:
async function выполнитьЗапрос() {
  try {
    const ответ = await создатьЗапросСТаймаутом();
    const данные = await ответ.json();
    console.log('Данные получены:', данные);
  } catch (ошибка) {
    if (ошибка.name === 'AbortError') {
      console.log('Запрос был отменён');
    } else {
      console.error('Ошибка:', ошибка);
    }
  }
}

Объяснение: Иногда сервер зависает и не отвечает. Чтобы программа не ждала вечно, мы используем AbortController. Он позволяет отменить запрос через определённое время или по другому событию.

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаком с Python, тебе будет проще. Давай сравним, как выглядит работа с сетью на двух языках.

Аспект TypeScript (fetch) Python (requests)
Библиотека Встроенный fetch (не нужны дополнительные библиотеки) Нужно установить: pip install requests
GET-запрос await fetch(url) requests.get(url)
POST с JSON fetch(url, { method: 'POST', body: JSON.stringify(data) }) requests.post(url, json=data)
Преобразование ответа await response.json() response.json()
Обработка ошибок try/catch + проверка response.ok try/except + response.raise_for_status()
Заголовки headers: { 'Authorization': 'Bearer token' } headers={'Authorization': 'Bearer token'}
Типизация Есть (интерфейсы, дженерики) Нет встроенной (можно использовать mypy или pydantic)
Асинхронность По умолчанию асинхронный (async/await) Синхронный по умолчанию (нужен aiohttp для async)

Главное отличие: TypeScript работает в браузере и Node.js, поэтому он асинхронный «из коробки». Python — синхронный, но есть библиотеки для асинхронной работы. TypeScript требует явного преобразования в JSON (JSON.stringify и response.json()), а Python делает это автоматически.

Пример на Python для сравнения:

# Python код для сравнения
import requests

response = requests.get('https://jsonplaceholder.typicode.com/todos/1')
data = response.json()
print(data['title'])

# POST запрос
new_todo = {'title': 'Новая задача', 'completed': False}
response = requests.post('https://jsonplaceholder.typicode.com/todos', json=new_todo)
print(response.json())

Типичные ошибки и подводные камни

Даже опытные программисты иногда попадаются на этих граблях. Давай разберём самые частые ошибки.

Ошибка 1: Забыли про await

// ❌ НЕПРАВИЛЬНО
function получитьДанные() {
  const ответ = fetch('https://api.example.com/data'); // Ответ — Promise, а не данные!
  console.log(ответ.data); // undefined
}

// ✅ ПРАВИЛЬНО
async function получитьДанные() {
  const ответ = await fetch('https://api.example.com/data');
  const данные = await ответ.json();
  console.log(данные);
}

Почему: fetch возвращает Promise (обещание), а не сразу данные. Без await ты работаешь с обещанием, а не с результатом.

Ошибка 2: Не проверяем статус ответа

// ❌ НЕПРАВИЛЬНО
async function опасныйЗапрос() {
  const ответ = await fetch('https://api.example.com/несуществующий_ресурс');
  const данные = await ответ.json(); // Сервер вернул 404, но мы всё равно пытаемся прочитать JSON
  console.log(данные); // Может упасть с ошибкой
}

// ✅ ПРАВИЛЬНО
async function безопасныйЗапрос() {
  const ответ = await fetch('https://api.example.com/несуществующий_ресурс');
  if (!ответ.ok) {
    throw new Error(`HTTP error! status: ${ответ.status}`);
  }
  const данные = await ответ.json();
  console.log(данные);
}

Почему: fetch не считает ошибкой 404 или 500. Он считает ошибкой только если запрос вообще не удалось отправить (нет интернета, сервер недоступен). Статусы 4xx и 5xx нужно проверять вручную.

Ошибка 3: Забыли про CORS

CORS (Cross-Origin Resource Sharing) — это защита браузера. Если твой сайт на mysite.com пытается сделать запрос к api.othersite.com, браузер может заблокировать запрос.

Ошибка CORS выглядит так: «Access to fetch at '...' from origin '...' has been blocked by CORS policy». Это не ошибка твоего кода — это сервер не разрешает запросы с других сайтов.

Решение: Убедись, что сервер, к которому ты обращаешься, разрешает CORS. Если ты разрабатываешь и сервер, и клиент — настрой CORS на сервере. Если нет — используй прокси или обратись к владельцу API.

Ошибка 4: Неправильный Content-Type

// ❌ НЕПРАВИЛЬНО
const ответ = await fetch('https://api.example.com/users', {
  method: 'POST',
  body: { name: 'Иван' } // Объект, а не строка!
});

// ✅ ПРАВИЛЬНО
const ответ = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'Иван' }) // Превращаем в строку
});

Почему: По сети можно передавать только строки или бинарные данные. Объект нужно сначала превратить в строку с помощью JSON.stringify().

Ошибка 5: Игнорирование таймаутов

По умолчанию fetch не имеет таймаута. Если сервер завис, твоя программа будет ждать вечно. Всегда используй AbortController для установки разумного таймаута (обычно 10-30 секунд).

Лучшие практики и советы профессионалов

1. Создай единый клиент для API

Не разбрасывай fetch по всему коду. Создай один класс или функцию, которая будет делать все запросы:

class ApiClient {
  private baseUrl: string;
  private token: string | null = null;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  setToken(token: string) {
    this.token = token;
  }

  private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;
    const headers: HeadersInit = {
      'Content-Type': 'application/json',
      ...options.headers,
    };

    if (this.token) {
      headers['Authorization'] = `Bearer ${this.token}`;
    }

    const response = await fetch(url, {
      ...options,
      headers,
    });

    if (!response.ok) {
      throw new ApiError(response.status, await response.text());
    }

    return response.json();
  }

  async get<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: 'GET' });
  }

  async post<T>(endpoint: string, data: unknown): Promise<T> {
    return this.request<T>(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async delete<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: 'DELETE' });
  }
}

// Использование:
const api = new ApiClient('https://api.example.com');
api.setToken('мой_токен');
const users = await api.get<User[]>('/users');

2. Всегда обрабатывай ошибки

Сеть — ненадёжна. Всегда используй try/catch. Показывай пользователю понятные сообщения, а не технические детали.

3. Используй интерфейсы для ответов

Типизация — главное преимущество TypeScript. Всегда создавай интерфейсы для данных, которые возвращает API.

4. Кешируй данные, когда это возможно

Если данные меняются редко (список стран, курсы валют), сохраняй их локально и обновляй только по необходимости. Это сэкономит трафик и ускорит приложение.

5. Не отправляй лишние данные

Запрашивай только те поля, которые тебе нужны. Многие API поддерживают параметры fields или select.

6. Используй HTTP-перехватчики для логирования

В разработке полезно видеть, какие запросы отправляются и какие ответы приходят. Используй инструменты разработчика в браузере (вкладка Network) или специальные библиотеки.

Вопросы для самопроверки

  1. Вопрос: Что такое HTTP-метод GET и для чего он используется?

    Ответ: GET — это метод для получения данных с сервера. Он не меняет состояние сервера. Аналогия: «Покажи мне меню ресторана». GET-запросы можно повторять сколько угодно раз без последствий.

  2. Вопрос: Какая разница между кодами ответа 200 и 201?

    Ответ: 200 (OK) — запрос выполнен успешно, сервер вернул данные. 201 (Created) — запрос выполнен успешно, и на сервере был создан новый ресурс (обычно после POST-запроса).

  3. Вопрос: Зачем нужен await перед fetch()?

    Ответ: fetch() возвращает Promise (обещание), а не сразу результат. await приостанавливает выполнение функции, пока Promise не выполнится, и возвращает результат. Без await мы получим Promise, а не ответ сервера.

  4. Вопрос: