Перейти к содержанию

API Модули — Полный код

Уровень: L3 (deep-dive) | ← Назад к API Layer

1. Auth API

Файл: frontend/src/api/auth.ts

Авторизация, регистрация, управление профилем.

import { apiClient } from './client';
import type {
  ChangePasswordRequest,
  LoginRequest,
  RefreshRequest,
  RegisterRequest,
  TokenResponse,
  User,
  UserUpdate,
} from '../types/api';

export const authApi = {
  // Вход по логину и паролю → пара токенов
  login: (data: LoginRequest) =>
    apiClient.post<TokenResponse>('/auth/login', data).then((r) => r.data),

  // Регистрация нового пользователя → пара токенов
  register: (data: RegisterRequest) =>
    apiClient.post<TokenResponse>('/auth/register', data).then((r) => r.data),

  // Обновление access token по refresh token
  refresh: (refreshToken: string) =>
    apiClient
      .post<TokenResponse>('/auth/refresh', { refresh_token: refreshToken } satisfies RefreshRequest)
      .then((r) => r.data),

  // Получить текущего пользователя
  getMe: () =>
    apiClient.get<User>('/users/me').then((r) => r.data),

  // Обновить профиль (имя, аватар)
  updateMe: (data: UserUpdate) =>
    apiClient.patch<User>('/users/me', data).then((r) => r.data),

  // Смена пароля
  changePassword: (data: ChangePasswordRequest) =>
    apiClient.post<void>('/users/me/password', data).then(() => undefined),
};

Типы запросов/ответов

interface LoginRequest { login: string; password: string; }
interface RegisterRequest { name: string; email: string; phone?: string; password: string; }
interface RefreshRequest { refresh_token: string; }
interface TokenResponse { access_token: string; refresh_token: string; expires_in: number; }
interface User { id: string; name: string; email: string; phone?: string; avatar_media_id?: string; created_at: string; }
interface UserUpdate { name?: string; avatar_media_id?: string; }
interface ChangePasswordRequest { current_password: string; new_password: string; }

2. Items API

Файл: frontend/src/api/items.ts

CRUD операции с вещами гардероба.

import { apiClient } from './client';
import type { Item, ItemCreate, ItemUpdate, ItemList, ItemListParams } from '../types/api';

export const itemsApi = {
  // Список вещей с фильтрацией и пагинацией
  list: (params?: ItemListParams) =>
    apiClient.get<ItemList>('/items', { params }).then((r) => r.data),

  // Получить одну вещь по ID
  get: (id: string) =>
    apiClient.get<Item>(`/items/${id}`).then((r) => r.data),

  // Создать новую вещь
  create: (data: ItemCreate) =>
    apiClient.post<Item>('/items', data).then((r) => r.data),

  // Обновить вещь (partial update)
  update: (id: string, data: ItemUpdate) =>
    apiClient.patch<Item>(`/items/${id}`, data).then((r) => r.data),

  // Удалить вещь
  delete: (id: string) =>
    apiClient.delete(`/items/${id}`).then((r) => r.data),
};

Типы

interface ItemListParams {
  category_id?: number; color_id?: number; season?: Season;
  style_node_id?: number; slot_id?: number; status?: ItemStatus;
  include_embeddings?: boolean; limit?: number; offset?: number;
}
interface ItemCreate { title?: string; category_id: number; primary_color_id?: number; primary_media_id?: string; style_node_ids?: number[]; }
interface ItemUpdate { title?: string; category_id?: number; primary_color_id?: number; primary_media_id?: string; status?: ItemStatus; excluded_from_recommendations?: boolean; style_node_ids?: number[]; }
interface ItemList { items: Item[]; total: number; limit: number; offset: number; }

3. Outfits API

Файл: frontend/src/api/outfits.ts

CRUD образов, сохранение, генерация названий, шеринг.

import { apiClient } from './client';
import type {
  Outfit, OutfitCreate, OutfitSuggestTitleRequest, OutfitSuggestTitleResponse,
  OutfitUpdate, OutfitList, OutfitListParams, ShareLink, ShareFormat,
} from '../types/api';

export const outfitsApi = {
  // Список образов с фильтрацией
  list: (params?: OutfitListParams) =>
    apiClient.get<OutfitList>('/outfits', { params }).then((r) => r.data),

  // Получить один образ по ID
  get: (id: string) =>
    apiClient.get<Outfit>(`/outfits/${id}`).then((r) => r.data),

  // Создать новый образ
  create: (data: OutfitCreate) =>
    apiClient.post<Outfit>('/outfits', data).then((r) => r.data),

  // Предложить название для образа (ML на бэкенде)
  suggestTitle: (data: OutfitSuggestTitleRequest) =>
    apiClient.post<OutfitSuggestTitleResponse>('/outfits/suggest-title', data).then((r) => r.data),

  // Обновить образ
  update: (id: string, data: OutfitUpdate) =>
    apiClient.patch<Outfit>(`/outfits/${id}`, data).then((r) => r.data),

  // Удалить образ
  delete: (id: string) =>
    apiClient.delete(`/outfits/${id}`).then((r) => r.data),

  // Сохранить чужой образ в коллекцию
  save: (id: string) =>
    apiClient.post(`/outfits/${id}/save`).then((r) => r.data),

  // Убрать из сохранённых
  unsave: (id: string) =>
    apiClient.delete(`/outfits/${id}/save`).then((r) => r.data),

  // Получить сохранённые образы
  saved: (params?: { origin_type?: 'own' | 'external'; limit?: number; offset?: number }) =>
    apiClient.get<OutfitList>('/outfits/saved', { params }).then((r) => r.data),

  // Получить похожие образы (серверный поиск по слотам)
  similar: (id: string, params?: { slot_id?: number; limit?: number; offset?: number }) =>
    apiClient.get<OutfitList>(`/outfits/${id}/similar`, { params }).then((r) => r.data),

  // Создать публичную ссылку для шеринга
  share: (id: string, format: ShareFormat) =>
    apiClient.post<ShareLink>(`/outfits/${id}/share`, { format }).then((r) => r.data),
};

4. Feed API

Файл: frontend/src/api/feed.ts

Лента образов с фильтрацией по погоде, стилю, цвету.

import { apiClient } from './client';
import type { FeedList, FeedParams } from '../types/api';

export const feedApi = {
  // Получить ленту образов с фильтрами
  list: (params?: FeedParams) =>
    apiClient.get<FeedList>('/feed', { params }).then((r) => r.data),
};

Типы фильтров ленты

interface FeedParams {
  origin_type?: 'own' | 'external';
  occasion?: string; style?: string; weather?: string;
  color_id?: number; season?: Season; category_id?: number;
  slot_id?: number; collection?: string;
  limit?: number; offset?: number; cursor?: number;
}
interface FeedList extends OutfitList { next_offset?: number; }

5. Media API

Файл: frontend/src/api/media.ts

Загрузка медиафайлов с автоматической компрессией.

import { apiClient } from './client';
import type { Media, MediaKind, MediaUploadResponse } from '../types/api';
import { compressImage } from '../utils/compressImage';

// Форматирование размера файла для логов
function formatFileSize(bytes: number): string {
  if (bytes < 1024 * 1024) {
    return `${Math.max(1, Math.round(bytes / 1024))}KB`;
  }
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}

export const mediaApi = {
  // Загрузка файла с компрессией в зависимости от типа
  upload: async (file: File, kind: MediaKind = 'other') => {
    // Профили компрессии для разных типов медиа
    const compressOpts =
      kind === 'item_photo'
        ? { maxWidth: 896, maxHeight: 896, quality: 0.8, format: 'jpeg' as const }
        : kind === 'avatar'
          ? { maxWidth: 512, maxHeight: 512, quality: 0.85, format: 'jpeg' as const }
          : null;  // Для остальных типов — без компрессии

    // Применить компрессию если указан профиль
    const uploadFile = compressOpts ? await compressImage(file, compressOpts) : file;

    // Логирование результата компрессии
    if (kind === 'item_photo' || kind === 'avatar') {
      console.info(
        `[media] compress: ${formatFileSize(file.size)} => ${formatFileSize(uploadFile.size)} (${uploadFile.type})`
      );
    }

    // Отправка как multipart/form-data
    const formData = new FormData();
    formData.append('file', uploadFile);
    formData.append('kind', kind);
    return apiClient
      .post<MediaUploadResponse>('/media', formData, {
        headers: { 'Content-Type': 'multipart/form-data' },
      })
      .then((r) => r.data);
  },

  // Получить метаданные медиа по ID
  get: (id: string) =>
    apiClient.get<Media>(`/media/${id}`).then((r) => r.data),
};

Профили компрессии

Тип Размер Quality Формат
item_photo 896×896 0.8 JPEG
avatar 512×512 0.85 JPEG
Остальные Без изменений

6. Share API

Файл: frontend/src/api/share.ts

Публичные ссылки на образы, импорт чужих образов.

import { apiClient } from './client';
import type { Outfit, SharedOutfitList } from '../types/api';

export const shareApi = {
  // Получить образ по публичному токену (без авторизации)
  getByToken: (token: string) =>
    apiClient.get<Outfit>(`/share/${token}`).then((r) => r.data),

  // Импортировать (claim) чужой образ к себе
  claim: (token: string) =>
    apiClient.post<Outfit>(`/share/${token}`).then((r) => r.data),

  // Получить свои расшаренные образы
  getMySharedOutfits: (params?: { limit?: number; offset?: number }) =>
    apiClient
      .get<SharedOutfitList>('/users/me/shared-outfits', { params })
      .then((r) => r.data),

  // Отозвать публичную ссылку
  revoke: (token: string) =>
    apiClient.delete(`/share/${token}`).then((r) => r.data),
};

7. Reference API

Файл: frontend/src/api/reference.ts

Справочные данные (кешируются в localStorage для offline).

import { apiClient } from './client';
import type { Color, ItemCategory, Slot, StyleNode, Collection, StyleNodeKind } from '../types/api';

export const referenceApi = {
  // Все цвета
  colors: () =>
    apiClient.get<Color[]>('/reference/colors').then((r) => r.data),

  // Категории вещей (с фильтрацией по parent_id или slot_id)
  categories: (params?: { parent_id?: number; slot_id?: number }) =>
    apiClient.get<ItemCategory[]>('/reference/categories', { params }).then((r) => r.data),

  // Слоты (верх, низ, обувь и т.д.)
  slots: () =>
    apiClient.get<Slot[]>('/reference/slots').then((r) => r.data),

  // Стили (паттерн, эстетика, фит, сезон)
  styles: (params?: { kind?: StyleNodeKind; parent_id?: number }) =>
    apiClient.get<StyleNode[]>('/reference/styles', { params }).then((r) => r.data),

  // Коллекции (тематические подборки)
  collections: () =>
    apiClient.get<Collection[]>('/reference/collections').then((r) => r.data),
};

Типы справочников

interface Color { id: number; name: string; }
interface ItemCategory { id: number; name: string; parent_id?: number; default_slot_id?: number; }
interface Slot { id: number; code: string; name: string; order_index: number; min_layers: number; max_layers: number; }
type StyleNodeKind = 'pattern' | 'aesthetic' | 'fit' | 'season' | 'other';
interface StyleNode { id: number; name: string; kind: StyleNodeKind; parent_id?: number; }
interface Collection { code: string; name: string; description?: string; }