Как я строю слой запросов
Главная мысль: слой запросов должен быть самостоятельным модулем, который не знает ничего про компоненты, страницы и UI.
Задача, которую решает такой подход
Когда запросы разбросаны по компонентам, очень быстро начинается хаос:
- везде разная обработка ошибок
- один и тот же endpoint дублируется в разных файлах
- сложно переиспользовать логику между проектами
Я стараюсь этого избегать через простую и плоскую структуру.
Структура
Базовая схема
/services/
├── request.ts
├── client.ts
└── requests/
├── products/
│ ├── routes.ts
│ └── index.ts
└── users/
├── routes.ts
└── index.ts
Здесь есть три уровня:
request.ts— низкоуровневый транспорт (GET/POST/PUT и т.д.)requests/*— доменные модули (products,users,orders)client.ts— единая точка входа (ApiClient)
Request-слой
В request.ts лежит универсальная функция apiRequest, которая:
- собирает URL с query-параметрами
- ставит timeout
- нормализует ошибки
- возвращает либо JSON, либо text
Пример из проекта:
export const request = {
get: <T>(url: string, options?: Omit<ApiRequestOptions, "method">) =>
apiRequest<T>(url, { ...options, method: "GET" }),
post: <T>(url: string, body?: unknown, options?: Omit<ApiRequestOptions, "method" | "body">) =>
apiRequest<T>(url, { ...options, method: "POST", body }),
put: <T>(url: string, body?: unknown, options?: Omit<ApiRequestOptions, "method" | "body">) =>
apiRequest<T>(url, { ...options, method: "PUT", body })
}
Доменные запросы по папкам
Дальше каждый источник данных живёт отдельно. Например, products/index.ts:
// products/index.ts
export const getProducts = async (): Promise<Product[]> => {
try {
return await request.get<Product[]>(routes.list())
} catch {
return []
}
}
export const getProduct = async (id: number): Promise<Product | null> => {
try {
return await request.get<Product>(routes.byId(id))
} catch {
return null
}
}
Отдельно там же лежит routes.ts, чтобы URL не размазывались по коду.
// products/routes.ts
export const routes = {
list: () => `/products`,
byId: (id: number) => `/products/${id}`
}
Единый API-клиент
Когда модулей становится несколько, собираю их в общий клиент:
// services/client.ts
import * as products from "@/services/requests/products/index"
import * as users from "@/services/requests/users/index"
export const ApiClient = {
products,
users
}
Это даёт предсказуемую точку входа: ApiClient.<domain>.<method>.
Как это используется в UI
Пример использования в компоненте:
// components/ProductsList.vue
import { ApiClient } from "@/services/client"
const products = ref([])
const loading = ref(true)
onMounted(async () => {
try {
products.value = await ApiClient.products.getProducts()
} finally {
loading.value = false
}
})
Компонент не знает про URL, headers и fetch-детали. Он работает только с доменным методом.
Минимальный набор
Если нужен стабильный и переиспользуемый слой запросов, то минимальный набор такой:
- общий
requestс единым поведением - папки
requests/<domain>с методами под конкретную сущность - единый
ApiClientкак публичный API для приложения
Когда проект растёт, это разделение позволяет не смешивать бизнес-логику и представление. Этого уже достаточно, чтобы код не расползался и оставался читаемым даже по мере роста проекта.