Работа с сетью и HTTP API в TypeScript: подробное руководство
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты сидишь в уютном кафе. Ты — это твоя программа (клиент). На стойке стоит официант — это 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) или специальные библиотеки.
Вопросы для самопроверки
-
Вопрос: Что такое HTTP-метод GET и для чего он используется?
Ответ: GET — это метод для получения данных с сервера. Он не меняет состояние сервера. Аналогия: «Покажи мне меню ресторана». GET-запросы можно повторять сколько угодно раз без последствий.
-
Вопрос: Какая разница между кодами ответа 200 и 201?
Ответ: 200 (OK) — запрос выполнен успешно, сервер вернул данные. 201 (Created) — запрос выполнен успешно, и на сервере был создан новый ресурс (обычно после POST-запроса).
-
Вопрос: Зачем нужен
awaitпередfetch()?Ответ:
fetch()возвращает Promise (обещание), а не сразу результат.awaitприостанавливает выполнение функции, пока Promise не выполнится, и возвращает результат. Безawaitмы получим Promise, а не ответ сервера. -
Вопрос: