Практика 12. Практическая работа 12. Сетевой слой: API-клиент, интерсепторы, JWT
Раздел 2. Привязка к лекции 12 («Сети, часть 1»). Работа практико-кодовая: строим переиспользуемый сетевой слой и проверяем его на реальных запросах.
Цели работы
- Построить единый API-клиент — обёртку над
fetch(илиaxios) с базовым URL и общими заголовками. - Реализовать интерсептор запроса: автоматическая подстановка JWT в заголовок
Authorization. - Реализовать интерсептор ответа: парсинг JSON и единая обработка кодов ошибок.
- Закрепить теорию: сравнить размер JSON-ответа, обсудить уместность Protobuf, обзорно посмотреть на заголовки HTTP/2.
Коротко о теории
Зачем нужен сетевой слой. Если разбросать fetch по экранам, то URL,
заголовки и обработка ошибок дублируются повсюду — при смене API придётся править
десятки файлов. Весь сетевой код выносят в один модуль — клиент API.
Структура: компонент → сервис → клиент API (+интерсепторы) → сеть.
Интерсептор — перехватчик, встроенный в каждый запрос или ответ: в запросе подставляет токен, в ответе централизованно обрабатывает ошибки.
JWT — самодостаточный токен вида header.payload.signature. Передаётся в
заголовке Authorization: Bearer <token>; сервер проверяет подпись и срок
действия exp, не храня сессию (stateless).
Задание
Работаем в папке api/. Для тестовых запросов подойдёт публичный API
https://jsonplaceholder.typicode.com или ваш учебный бэкенд с авторизацией.
Шаг 1. Хранилище токена
Вынесем доступ к токену в отдельный модуль, чтобы клиент не знал, где он лежит
(в учебном примере — переменная в памяти, в реальном приложении —
expo-secure-store или AsyncStorage).
let accessToken = null;export const setToken = (t) => { accessToken = t; };export const getToken = () => accessToken;export const clearToken = () => { accessToken = null; };Шаг 2. Базовый клиент на fetch
Клиент инкапсулирует базовый URL, таймаут и общие заголовки. Ядро — приватная
функция request, на её основе строятся get и post.
import { getToken, clearToken } from './tokenStore';
const BASE_URL = 'https://jsonplaceholder.typicode.com';const TIMEOUT = 10000;
// Класс ошибки, чтобы наверху различать сетевые сбоиexport class ApiError extends Error { constructor(message, status, data) { super(message); this.name = 'ApiError'; this.status = status; this.data = data; }}Шаг 3. Интерсептор запроса (подстановка JWT)
Перед отправкой собираем заголовки и, если токен есть, добавляем Authorization.
// api/client.js (продолжение)function buildHeaders(custom = {}) { const headers = { 'Content-Type': 'application/json', ...custom }; const token = getToken(); // интерсептор запроса if (token) headers.Authorization = `Bearer ${token}`; return headers;}Шаг 4. Интерсептор ответа (парсинг JSON и обработка ошибок)
Единая точка, где разбираем тело и реагируем на коды состояния.
// api/client.js (продолжение)async function handleResponse(response) { const text = await response.text(); const data = text ? JSON.parse(text) : null; // парсинг JSON
if (response.ok) return data; // 2xx — успех
switch (response.status) { // обработка ошибок case 401: clearToken(); // токен истёк — сбрасываем throw new ApiError('Не авторизован', 401, data); case 403: throw new ApiError('Доступ запрещён', 403, data); case 404: throw new ApiError('Ресурс не найден', 404, data); default: const msg = response.status >= 500 ? 'Ошибка сервера' : 'Ошибка запроса'; throw new ApiError(msg, response.status, data); }}Шаг 5. Функция request и методы get/post
request собирает запрос, навешивает таймаут через AbortController и
пропускает ответ через интерсептор.
// api/client.js (продолжение)async function request(path, { method = 'GET', body, headers } = {}) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), TIMEOUT); try { const response = await fetch(`${BASE_URL}${path}`, { method, headers: buildHeaders(headers), body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }); return await handleResponse(response); } catch (err) { if (err.name === 'AbortError') throw new ApiError('Превышено время ожидания', 0, null); throw err; } finally { clearTimeout(timer); }}
export const apiClient = { get: (path, headers) => request(path, { method: 'GET', headers }), post: (path, body, headers) => request(path, { method: 'POST', body, headers }),};Шаг 6. Пример вызова
import { apiClient } from './client';
export const userService = { getUser: (id) => apiClient.get(`/users/${id}`), createPost: (data) => apiClient.post('/posts', data),};
// где-то в коде экранаsetToken('eyJhbGciOi...'); // обычно после логинаtry { const user = await userService.getUser(1); console.log('Имя:', user.name);} catch (e) { if (e instanceof ApiError && e.status === 401) console.warn('Нужно заново войти'); else console.error('Сбой сети:', e.message);}Шаг 7 (по желанию). Типизация TypeScript
interface User { id: number; name: string; email: string; }
async function request<T>(path: string, opts?: RequestOptions): Promise<T> { /* ... */ }const user = await apiClient.get<User>('/users/1'); // user выводится как UserДополнительное задание: форматы и HTTP/2
-
Размер JSON. Запросите
GET /users/1, сохраните ответ в файл и посмотрите его размер. Прикиньте, какую долю занимают повторяющиеся имена полей ("name","email"…). Сделайте вывод о читаемости против компактности. -
Где уместен Protobuf. Опишите 2–3 строками, когда бинарный формат оправдан (высокая нагрузка, gRPC, большие объёмы по медленной сети) и почему для обычного REST проще остаться на JSON.
-
Заголовки HTTP/2 (обзорно). Выполните в терминале и сравните вывод:
Окно терминала curl -I --http2 https://jsonplaceholder.typicode.com/users/1Обратите внимание: статус показан как
HTTP/2 200, заголовки в нижнем регистре и сжимаются механизмом HPACK, а запросы по одному соединению идут параллельно (мультиплексирование) — в отличие от очереди в HTTP/1.1.
Критерии оценки
| Критерий | Вес |
|---|---|
| Клиент с базовым URL, таймаутом и общими заголовками | 20% |
Интерсептор запроса: подстановка JWT в Authorization | 20% |
| Интерсептор ответа: парсинг JSON и обработка кодов ошибок | 20% |
Методы get/post и сервис поверх клиента | 15% |
Рабочий пример вызова с обработкой ApiError | 15% |
| Доп. задание: сравнение JSON/Protobuf и заголовки HTTP/2 | 10% |
Вопросы для самопроверки
- Почему вызовы
fetchне размещают прямо в компонентах экранов? Какую проблему решает выделенный клиент API? - Что делает интерсептор запроса и что — интерсептор ответа? Приведите по примеру задачи для каждого.
- В каком заголовке и в каком формате передаётся JWT? Что сервер с ним делает?
- Как ваш клиент реагирует на код
401? Почему именно так? - Зачем нужен
AbortControllerи таймаут в мобильной сети? Когда переход с JSON на Protocol Buffers оправдан, а когда избыточен?
Ресурсы
- MDN: Fetch API — https://developer.mozilla.org/ru/docs/Web/API/Fetch_API
- MDN: AbortController — https://developer.mozilla.org/ru/docs/Web/API/AbortController
- axios (interceptors) — https://axios-http.com/docs/interceptors
- JWT — https://jwt.io/introduction
- Protocol Buffers — https://protobuf.dev/
- Expo SecureStore — https://docs.expo.dev/versions/latest/sdk/securestore/