Skip to content

Commit 9150dab

Browse files
committed
chore: refactor API lib
1 parent 7bc9e09 commit 9150dab

10 files changed

Lines changed: 94 additions & 114 deletions

File tree

.claude/rules/architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ Each feature follows feature slice architecture patterns with four layers:
5252

5353
- **components/** - UI components, presentational and decoupled from business logic (application) and router state. Data access is only through `providers/`.
5454
- **application/** - Business logic, portable state management (stores, FSMs, form validation), custom hooks. Should not depend on router state or external APIs directly (only through `providers/`).
55-
- **providers/** - Data fetching, external APIs, platform/SDK interactions, commands/mutations, and API type definitions (DTOs). Library-specific code (React Query, SWR, etc.) must not leak beyond this layer.
55+
- **providers/** - Hook composition and data access gateway for the feature slice. Exposes query hooks, mutations, loaders, and DTOs sourced from `src/lib/api/`. Library-specific code (React Query, etc.) must not leak beyond this layer.
5656
- **models/** - Domain type definitions, utilities, and type mapping functions.
5757

5858
**Dependency rule:** `components/` and `application/` import from `models/` and `providers/`. `providers/` and `models/` have no internal feature dependencies.
5959

6060
## API Layer
6161

62-
`src/lib/api/` is the global home for all HTTP logic: query/mutation hooks, loaders, query keys, and DTOs, organised by resource. Feature `providers/` do not implement API logic — they re-export selectively from `src/lib/api/` to decouple feature internals from the global API structure. New API logic always goes in `src/lib/api/` first, then gets exposed through the relevant feature's `providers/`.
62+
`src/lib/api/` is the global home for all HTTP logic: `queryOptions` factories, loaders, mutation hooks, query keys, and DTOs, organised by resource. Query files expose `queryOptions` factories (no `useQuery` hooks — hook composition belongs in `providers/`). Feature `providers/` compose hooks on top of those factories and re-export them for feature slice. New API logic always goes in `src/lib/api/` first, then gets exposed through the relevant feature's `providers/`.
6363

6464
## Key Patterns
6565

.github/instructions/architecture.instructions.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ Each feature follows feature slice architecture patterns with four layers:
4949

5050
- **components/** - UI components, presentational and decoupled from business logic (application) and router state. Data access is only through `providers/`.
5151
- **application/** - Business logic, portable state management (stores, FSMs, form validation), custom hooks. Should not depend on router state or external APIs directly (only through `providers/`).
52-
- **providers/** - Data fetching, external APIs, platform/SDK interactions, commands/mutations, and API type definitions (DTOs). Library-specific code (React Query, SWR, etc.) must not leak beyond this layer.
52+
- **providers/** - Hook composition and data access gateway for the feature slice. Exposes query hooks, mutations, loaders, and DTOs sourced from `src/lib/api/`. Library-specific code (React Query, etc.) must not leak beyond this layer.
5353
- **models/** - Domain type definitions, utilities, and type mapping functions.
5454

5555
**Dependency rule:** `components/` and `application/` import from `models/` and `providers/`. `providers/` and `models/` have no internal feature dependencies.
5656

57-
## API Layer
57+
### API Layer
5858

59-
`src/lib/api/` is the global home for all HTTP logic: query/mutation hooks, loaders, query keys, and DTOs, organised by resource. Feature `providers/` do not implement API logic — they re-export selectively from `src/lib/api/` to decouple feature internals from the global API structure. New API logic always goes in `src/lib/api/` first, then gets exposed through the relevant feature's `providers/`.
59+
`src/lib/api/` is the global home for all HTTP logic: `queryOptions` factories, loaders, mutation hooks, query keys, and DTOs, organised by resource. Query files expose `queryOptions` factories (no `useQuery` hooks — hook composition belongs in `providers/`). Feature `providers/` compose hooks on top of those factories and re-export them for feature slice. New API logic always goes in `src/lib/api/` first, then gets exposed through the relevant feature's `providers/`.
6060

6161
### Key Patterns
6262

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
export {
2-
useCartProductsQuery,
3-
cartProductsLoader,
4-
} from "@/lib/api/carts/{cart-id}/cart-products-query";
1+
import { keepPreviousData } from "@tanstack/react-query";
2+
3+
import { cartProductsQuery } from "@/lib/api/carts/{cart-id}/cart-products-query";
4+
import { useQuery } from "@/lib/query";
5+
6+
export { cartProductsLoader } from "@/lib/api/carts/{cart-id}/cart-products-query";
7+
export { cartProductsQuery };
8+
9+
export const useCartProductsQuery = (cartId: string) =>
10+
useQuery({ ...cartProductsQuery(cartId), placeholderData: keepPreviousData });
Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
export {
2-
getProductQuery,
3-
useProductQuery,
4-
productLoader,
5-
} from "@/lib/api/products/{product-id}/product-query";
1+
import { productQuery } from "@/lib/api/products/{product-id}/product-query";
2+
import { useQuery } from "@/lib/query";
3+
4+
export { productLoader } from "@/lib/api/products/{product-id}/product-query";
5+
export { productQuery };
6+
7+
export const useProductQuery = (productId: string) =>
8+
useQuery(productQuery(productId));
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
export {
2-
useProductsQuery,
3-
productsLoader,
4-
} from "@/lib/api/products/products-list/products-list-query";
1+
import { productsQuery } from "@/lib/api/products/products-list/products-list-query";
2+
import { useQuery } from "@/lib/query";
3+
import type { QueryParams } from "@/types/query-params";
4+
5+
export { productsLoader } from "@/lib/api/products/products-list/products-list-query";
6+
export { productsQuery };
7+
8+
export const useProductsQuery = (params?: QueryParams) =>
9+
useQuery(productsQuery(params));
Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { UseQueryOptions } from "@tanstack/react-query";
1+
import { queryOptions } from "@tanstack/react-query";
22

3-
import { getProductQuery } from "@/lib/api/products/{product-id}/product-query";
3+
import type { ProductDto } from "@/lib/api/products/{product-id}/product-dto";
44
import { httpService } from "@/lib/http";
5-
import { queryClient, useQuery } from "@/lib/query";
5+
import { queryClient } from "@/lib/query";
66

77
import { cartQueryKeys } from "../cart-query-keys";
88

@@ -14,39 +14,32 @@ interface IResponse {
1414
products: CartProductDto[];
1515
}
1616

17-
const getCartProductsQuery = (cartId: string) => ({
18-
queryKey: cartQueryKeys.products(cartId),
19-
queryFn: async (): Promise<IResponse> => {
20-
const cart = await httpService.get<CartDto>(`carts/${cartId}`);
21-
22-
const productPromises = cart.products.map((product) =>
23-
getProductQuery(product.productId.toString()).queryFn()
24-
);
25-
26-
const products = await Promise.all(productPromises);
27-
28-
return {
29-
date: cart.date,
30-
products: products.map((product) => ({
31-
...product,
32-
quantity:
33-
cart.products.find(
34-
(cartProduct) => cartProduct.productId === product.id
35-
)?.quantity ?? 0,
36-
})),
37-
};
38-
},
39-
});
40-
41-
export const useCartProductsQuery = (
42-
cartId: string,
43-
options?: UseQueryOptions<IResponse>
44-
) => {
45-
return useQuery({
46-
...getCartProductsQuery(cartId),
47-
...options,
17+
const cartProductsQuery = (cartId: string) =>
18+
queryOptions({
19+
queryKey: cartQueryKeys.products(cartId),
20+
queryFn: async (): Promise<IResponse> => {
21+
const cart = await httpService.get<CartDto>(`carts/${cartId}`);
22+
23+
const products = await Promise.all(
24+
cart.products.map((product) =>
25+
httpService.get<ProductDto>(`products/${product.productId}`)
26+
)
27+
);
28+
29+
return {
30+
date: cart.date,
31+
products: products.map((product) => ({
32+
...product,
33+
quantity:
34+
cart.products.find(
35+
(cartProduct) => cartProduct.productId === product.id
36+
)?.quantity ?? 0,
37+
})),
38+
};
39+
},
4840
});
49-
};
5041

5142
export const cartProductsLoader = async (cartId: string) =>
52-
queryClient.ensureQueryData(getCartProductsQuery(cartId));
43+
queryClient.ensureQueryData(cartProductsQuery(cartId));
44+
45+
export { cartProductsQuery };
Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
1-
import type { UseQueryOptions } from "@tanstack/react-query";
1+
import { queryOptions } from "@tanstack/react-query";
22

33
import { httpService } from "@/lib/http";
4-
import { queryClient, useQuery } from "@/lib/query";
4+
import { queryClient } from "@/lib/query";
55

66
import { cartQueryKeys } from "../cart-query-keys";
77

88
import type { CartDto } from "./cart-dto";
99

10-
export const getCartQuery = (cartId: string) => ({
11-
queryKey: cartQueryKeys.detail(cartId),
12-
queryFn: (): Promise<CartDto> => httpService.get<CartDto>(`carts/${cartId}`),
13-
});
14-
15-
export const useCartQuery = (
16-
cartId: string,
17-
options?: UseQueryOptions<CartDto>
18-
) => {
19-
return useQuery({
20-
...getCartQuery(cartId),
21-
...options,
10+
export const cartQuery = (cartId: string) =>
11+
queryOptions({
12+
queryKey: cartQueryKeys.detail(cartId),
13+
queryFn: (): Promise<CartDto> =>
14+
httpService.get<CartDto>(`carts/${cartId}`),
2215
});
23-
};
2416

2517
export const cartLoader = async (cartId: string) =>
26-
queryClient.ensureQueryData(getCartQuery(cartId));
18+
queryClient.ensureQueryData(cartQuery(cartId));
Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { UseQueryOptions } from "@tanstack/react-query";
1+
import { queryOptions } from "@tanstack/react-query";
22

33
import { buildUrl } from "@/lib/build-url";
44
import { httpService } from "@/lib/http";
5-
import { queryClient, useQuery } from "@/lib/query";
5+
import { queryClient } from "@/lib/query";
66
import type { Meta } from "@/types/meta";
77
import type { QueryParams } from "@/types/query-params";
88

@@ -16,27 +16,20 @@ interface ICollection {
1616
meta: Meta;
1717
}
1818

19-
const getProductsQuery = (params: QueryParams = defaultParams) => ({
20-
queryKey: productsQueryKeys.list(params),
21-
queryFn: (): Promise<ICollection> =>
22-
httpService.get<ProductDto[]>(buildUrl("products", params)).then((res) => ({
23-
products: res,
24-
meta: {
25-
...params,
26-
total: 20,
27-
},
28-
})),
29-
});
30-
31-
export const useProductsQuery = (
32-
params: QueryParams = defaultParams,
33-
options?: Omit<UseQueryOptions<ICollection>, "queryKey" | "queryFn">
34-
) => {
35-
return useQuery({
36-
...getProductsQuery(params),
37-
...options,
19+
export const productsQuery = (params: QueryParams = defaultParams) =>
20+
queryOptions({
21+
queryKey: productsQueryKeys.list(params),
22+
queryFn: (): Promise<ICollection> =>
23+
httpService
24+
.get<ProductDto[]>(buildUrl("products", params))
25+
.then((res) => ({
26+
products: res,
27+
meta: {
28+
...params,
29+
total: 20,
30+
},
31+
})),
3832
});
39-
};
4033

4134
export const productsLoader = async (params: QueryParams = defaultParams) =>
42-
queryClient.ensureQueryData(getProductsQuery(params));
35+
queryClient.ensureQueryData(productsQuery(params));
Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
1-
import type { UseQueryOptions } from "@tanstack/react-query";
1+
import { queryOptions } from "@tanstack/react-query";
22

33
import { httpService } from "@/lib/http";
4-
import { queryClient, useQuery } from "@/lib/query";
4+
import { queryClient } from "@/lib/query";
55

66
import { productsQueryKeys } from "../products-query-keys";
77

88
import type { ProductDto } from "./product-dto";
99

10-
export const getProductQuery = (productId: string) => ({
11-
queryKey: productsQueryKeys.detail(productId),
12-
queryFn: (): Promise<ProductDto> =>
13-
httpService.get<ProductDto>(`products/${productId}`),
14-
});
15-
16-
export const useProductQuery = (
17-
productId: string,
18-
options?: UseQueryOptions<ProductDto>
19-
) => {
20-
return useQuery({
21-
...getProductQuery(productId),
22-
...options,
10+
export const productQuery = (productId: string) =>
11+
queryOptions({
12+
queryKey: productsQueryKeys.detail(productId),
13+
queryFn: (): Promise<ProductDto> =>
14+
httpService.get<ProductDto>(`products/${productId}`),
2315
});
24-
};
2516

2617
export const productLoader = async (productId: string) =>
27-
queryClient.ensureQueryData(getProductQuery(productId));
18+
queryClient.ensureQueryData(productQuery(productId));

src/pages/Products/index.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Button } from "@chakra-ui/react";
2-
import { keepPreviousData } from "@tanstack/react-query";
32
import { Settings } from "lucide-react";
43
import { useState } from "react";
54

@@ -19,9 +18,7 @@ const ProductsPage = () => {
1918
const t = useTranslations("pages.products");
2019

2120
const [params, setParams] = useState<QueryParams>(defaultParams);
22-
const { data, isFetching } = useProductsQuery(params, {
23-
placeholderData: keepPreviousData,
24-
});
21+
const { data, isFetching } = useProductsQuery(params);
2522

2623
const noMoreProducts = data.meta.total <= params.limit;
2724

0 commit comments

Comments
 (0)