Архитектура Feature-Sliced Design и настройка HTTP-клиента
Архитектура Feature-Sliced Design и настройка HTTP-клиента
Добро пожаловать в курс React Lab Survival. Если вы читаете это, значит, перед вами стоит задача сдать лабораторную работу, которая звучит как список требований к коммерческому проекту: авторизация с двумя токенами, сложная валидация, React Query и архитектура FSD. Звучит страшно, но мы разберем этого слона по частям.
Эта статья — фундамент. Мы не будем писать код «как попало», чтобы потом переписывать его в ночь перед дедлайном. Мы сразу настроим проект так, чтобы преподавателю было не к чему придраться в плане структуры.
Почему Feature-Sliced Design?
В маленьких проектах (например, To-Do List) мы привыкли делить код по типам файлов: папка components, папка hooks, папка pages. Это называется File-Type Architecture. Но когда проект растет, компоненты начинают путаться, импорты превращаются в паутину, и понять, какая кнопка к какой логике относится, становится невозможно.
Feature-Sliced Design (FSD) — это архитектурная методология, которая делит код не по типам файлов, а по бизнес-ценности. Она заставляет вас отвечать на вопрос: «Для чего нужен этот код?», а не «Что это за файл?».
Слои FSD
FSD делит приложение на слои. Это строгая иерархия: нижние слои не могут использовать верхние. Это спасает от циклических зависимостей.
Разберем слои снизу вверх (от самых независимых к самым сложным):
-
Shared (Общее) Самый нижний слой. Здесь лежит код, который не знает ничего о бизнесе вашего приложения. Это UI-кит (кнопки, инпуты), утилиты, конфигурация API, типы данных. Пример:
Button,Input,axiosInstance. -
Entities (Сущности) Бизнес-единицы, с которыми работает ваше приложение. Они содержат данные и отображение этих данных, но не содержат логику взаимодействия. Пример:
User(карточка профиля),Product(карточка товара). -
Features (Фичи) Слой действий. Здесь происходит взаимодействие пользователя с сущностями. Это то, что приносит ценность. Пример:
AuthByEmail(форма входа),AddToCart(кнопка добавления в корзину). -
Widgets (Виджеты) Самостоятельные блоки страницы, объединяющие сущности и фичи. Пример:
Header(логотип + меню + профиль),ProductList(список товаров с фильтрами). -
Pages (Страницы) Композиция виджетов, фич и сущностей для конкретного маршрута. Пример:
LoginPage,CatalogPage. -
App (Приложение) Инициализация. Здесь подключаются провайдеры, роутер, глобальные стили. Пример:
App.tsx,providers,styles.
Структура проекта для лабораторной
Исходя из требований вашей лабы (Auth + CRUD), структура папок в src будет выглядеть примерно так:
src/
├── app/ # Провайдеры, роутер, глобальные стили
├── pages/ # Страницы (Login, Register, Dashboard)
├── widgets/ # Header, Sidebar
├── features/ # Auth (формы входа/регистрации), CreateItem, DeleteItem
├── entities/ # Session (данные юзера), Item (сущность для CRUD)
└── shared/ # UI-kit, API config, hooks, constants
Главное правило FSD: модуль может импортировать только из модулей, находящихся на слоях ниже него.
Настройка HTTP-клиента (Axios)
Для работы с сетью мы будем использовать библиотеку Axios. Она является стандартом де-факто благодаря встроенной поддержке перехватчиков (interceptors), которые критически важны для реализации авторизации с обновлением токенов.
Установка
npm install axios
Создание инстанса
В FSD конфигурация API относится к слою Shared. Создадим файл src/shared/api/api.ts.
Мы не будем использовать глобальный объект axios, а создадим настроенный экземпляр (instance). Это позволит нам задать базовый URL и настройки один раз.
// src/shared/api/api.ts
import axios from 'axios';
// Базовый URL берем из переменных окружения
const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
export const api = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
Зачем нужны Interceptors (Перехватчики)?
В требованиях к лабе указано: «Авторизация с двумя токенами и автообновлением access». Это значит, что:
- К каждому запросу нужно автоматически добавлять
Access Token. - Если сервер ответил ошибкой 401 (Unauthorized), нужно попробовать обновить токены и повторить запрос.
Делать это вручную в каждом компоненте — безумие. Для этого и нужны перехватчики.
Request Interceptor (Перехватчик запроса)
Он срабатывает перед тем, как запрос улетит на сервер. Здесь мы добавляем токен.
// Добавляем в src/shared/api/api.ts
// Ключ, под которым токен лежит в localStorage
const ACCESS_TOKEN_KEY = 'access_token';
api.interceptors.request.use((config) => {
const token = localStorage.getItem(ACCESS_TOKEN_KEY);
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
Response Interceptor (Перехватчик ответа)
Он срабатывает, когда ответ пришел от сервера. Здесь мы ловим ошибки. Полную логику обновления токенов (Refresh Flow) мы реализуем в следующей статье, посвященной авторизации, но каркас подготовим сейчас.
// Добавляем в src/shared/api/api.ts
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Если ошибка 401 и мы еще не пытались обновить токен
if (error.response?.status === 401 && !error.config._isRetry) {
originalRequest._isRetry = true;
try {
// Здесь будет логика обновления токенов
// const response = await axios.post(...)
// localStorage.setItem(ACCESS_TOKEN_KEY, response.data.accessToken);
// Повторяем оригинальный запрос с новым токеном
return api.request(originalRequest);
} catch (e) {
// Если не вышло обновить токен — разлогиниваем юзера
console.error('Не авторизован');
}
}
throw error;
}
);
Интеграция с переменными окружения
Никогда не хардкодьте адреса API. Создайте в корне проекта файл .env:
VITE_API_URL=http://localhost:5000/api
И добавьте .env в .gitignore, чтобы не слить секретные ключи (если они будут) в репозиторий.
Резюме
Мы заложили прочный фундамент:
- FSD спасет нас от превращения кода в спагетти по мере роста функционала.
- Слои четко определяют, где должна лежать логика, а где — UI.
- Axios Instance в слое
Sharedготов к работе и автоматически подставляет токены.
В следующей статье мы реализуем полноценную систему авторизации, подключим React Query и заставим этот механизм работать как часы.