@wisemen/vue-core-api-utils 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,11 +4,11 @@ description: >
4
4
  Infinite pagination with useOffsetInfiniteQuery and useKeysetInfiniteQuery, offset vs keyset strategies determined by backend API, fetchNextPage, hasNextPage, isFetchingNextPage, data/meta result structure, proper page assembly.
5
5
  type: core
6
6
  library: vue-core-api-utils
7
- library_version: "0.0.3"
7
+ library_version: "1.2.0"
8
8
  sources:
9
- - "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/paginated-query.md"
10
9
  - "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/offsetInfiniteQuery.composable.ts"
11
10
  - "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/keysetInfiniteQuery.composable.ts"
11
+ - "wisemen-digital/wisemen-core:packages/web/api-utils/src/types/pagination.type.ts"
12
12
  subsystems:
13
13
  - "Offset Pagination"
14
14
  - "Keyset Pagination"
@@ -16,13 +16,13 @@ subsystems:
16
16
 
17
17
  # @wisemen/vue-core-api-utils — Writing Infinite Queries
18
18
 
19
- Paginate through large datasets with two strategies: offset-based (page/limit) for traditional pagination, or keyset-based (cursor) for real-time data and large datasets.
19
+ Paginate through large datasets with two strategies: offset-based (offset/limit) for traditional pagination, or keyset-based (cursor key) for real-time data and large datasets.
20
20
 
21
21
  **Choose your strategy based on what your backend API provides — not preference.**
22
22
 
23
23
  ## Setup
24
24
 
25
- ### Offset Pagination (page-based)
25
+ ### Offset Pagination (offset/limit-based)
26
26
 
27
27
  ```typescript
28
28
  import { ref, computed } from 'vue'
@@ -37,7 +37,7 @@ export function useContactList() {
37
37
  search: computed(() => search.value),
38
38
  },
39
39
  queryFn: (pagination) => ContactService.getAll({
40
- page: pagination.pageParam,
40
+ offset: pagination.offset,
41
41
  limit: pagination.limit,
42
42
  search: search.value,
43
43
  }),
@@ -45,7 +45,7 @@ export function useContactList() {
45
45
  }
46
46
  ```
47
47
 
48
- Pagination parameter `pageParam` starts at 0 and increments. Return results with `{ data: Contact[], meta: { page, limit, total } }`.
48
+ The `queryFn` receives `{ offset: number, limit: number }`. Offset starts at 0 and advances by `limit` for each next page. Return results with `{ data: Contact[], meta: { offset, limit, total } }`.
49
49
 
50
50
  ### Keyset Pagination (cursor-based)
51
51
 
@@ -63,14 +63,14 @@ export function useContactListKeyset() {
63
63
  },
64
64
  queryFn: (pagination) => ContactService.getAllKeyset({
65
65
  limit: pagination.limit,
66
- cursor: pagination.pageParam,
66
+ key: pagination.key,
67
67
  search: search.value,
68
68
  }),
69
69
  })
70
70
  }
71
71
  ```
72
72
 
73
- Pagination parameter `pageParam` is a cursor (string). Return results with `{ data: Contact[], meta: { next?: string } }` — the next cursor or undefined if no more pages.
73
+ The `queryFn` receives `{ key?: any, limit: number }`. The `key` is the cursor value from the previous page's `meta.next`, or `undefined` for the first page. Return results with `{ data: Contact[], meta: { next: unknown } }` — set `meta.next` to `null`/`undefined` when there are no more pages.
74
74
 
75
75
  ## Core Patterns
76
76
 
@@ -105,9 +105,56 @@ All pages are automatically concatenated into `data`. Access with `result.getVal
105
105
 
106
106
  Use `isFetchingNextPage` (not `isFetching`) to disable the load-more button only during pagination, not during initial load.
107
107
 
108
+ ### Custom page limit
109
+
110
+ ```typescript
111
+ useOffsetInfiniteQuery('contactList', {
112
+ params: { search: computed(() => search.value) },
113
+ limit: 50, // Default is 20
114
+ queryFn: (pagination) => ContactService.getAll({
115
+ offset: pagination.offset,
116
+ limit: pagination.limit,
117
+ }),
118
+ })
119
+ ```
120
+
121
+ Pass `limit` as a top-level option to override the default page size (20).
122
+
123
+ ## Response Structures
124
+
125
+ ### Offset pagination response
126
+
127
+ Your `queryFn` must return:
128
+ ```typescript
129
+ {
130
+ data: Contact[],
131
+ meta: {
132
+ offset: number, // Current offset
133
+ limit: number, // Items per page
134
+ total: number, // Total items across all pages
135
+ }
136
+ }
137
+ ```
138
+
139
+ The library uses `meta.offset + meta.limit >= meta.total` to determine if there are more pages.
140
+
141
+ ### Keyset pagination response
142
+
143
+ Your `queryFn` must return:
144
+ ```typescript
145
+ {
146
+ data: Contact[],
147
+ meta: {
148
+ next: unknown, // Cursor for the next page; null/undefined if no more pages
149
+ }
150
+ }
151
+ ```
152
+
153
+ The library uses `meta.next` as the `key` parameter for the subsequent page fetch.
154
+
108
155
  ## Common Mistakes
109
156
 
110
- ### CRITICAL: Import useInfiniteQuery from @tanstack/vue-query instead of factory
157
+ ### CRITICAL: Import useInfiniteQuery from @tanstack/vue-query instead of your api module
111
158
 
112
159
  ```typescript
113
160
  // ❌ Wrong: using TanStack directly
@@ -122,54 +169,75 @@ const { data, error } = useInfiniteQuery({
122
169
  ```
123
170
 
124
171
  ```typescript
125
- // ✅ Correct: use factory composable
172
+ // ✅ Correct: use the composable from your api module
126
173
  import { useOffsetInfiniteQuery } from '@/api'
127
174
 
128
175
  const { result, fetchNextPage, hasNextPage } = useOffsetInfiniteQuery('contactList', {
129
176
  params: { search: computed(() => '...') },
130
177
  queryFn: (pagination) => ContactService.getAll({
131
- page: pagination.pageParam,
178
+ offset: pagination.offset,
132
179
  limit: pagination.limit,
133
180
  }),
134
181
  })
135
182
  // Full AsyncResult wrapping, type safety, automatic error codes
136
183
  ```
137
184
 
138
- Direct TanStack import loses the factory's type safety, AsyncResult wrapping, and error code typing.
139
-
140
- Source: Library architecture — always use composables from `createApiUtils()` factory
185
+ Source: `src/composables/query/offsetInfiniteQuery.composable.ts`
141
186
 
142
187
  ### CRITICAL: Return paginated data without wrapping in data/meta structure
143
188
 
144
189
  ```typescript
145
190
  // ❌ Wrong: returning array directly
146
191
  queryFn: (pagination) => ContactService.getAll({
147
- page: pagination.pageParam,
192
+ offset: pagination.offset,
148
193
  limit: pagination.limit,
149
194
  })
150
- // Returns Contact[] directly instead of { data: Contact[], meta: {...} }
151
- // QueryClient doesn't know how to append pages; pages overwrite instead of concat
195
+ // Returns Contact[] directly instead of { data: Contact[], meta: { offset, limit, total } }
196
+ // Library can't determine if there are more pages infinite loop or stops too early
152
197
  ```
153
198
 
154
199
  ```typescript
155
200
  // ✅ Correct: return { data, meta } structure
156
201
  queryFn: (pagination) => ContactService.getAll({
157
- page: pagination.pageParam,
202
+ offset: pagination.offset,
158
203
  limit: pagination.limit,
159
- }).then(data => ({
160
- data,
161
- meta: {
162
- page: pagination.pageParam,
163
- limit: pagination.limit,
164
- total: 100, // Total count if available
165
- }
166
- }))
167
- // QueryClient knows how to append pages
204
+ })
205
+ // Where ContactService.getAll already returns { data: Contact[], meta: { offset, limit, total } }
206
+ ```
207
+
208
+ The library requires the `{ data, meta }` shape to know how to concatenate pages and when to stop.
209
+
210
+ Source: `src/types/pagination.type.ts` — `OffsetPaginationResponse`
211
+
212
+ ### HIGH: Use pageParam or cursor instead of offset/key
213
+
214
+ ```typescript
215
+ // ❌ Wrong: using old pageParam naming
216
+ queryFn: (pagination) => ContactService.getAll({
217
+ page: pagination.pageParam, // pageParam doesn't exist!
218
+ cursor: pagination.cursor, // cursor doesn't exist!
219
+ })
220
+ ```
221
+
222
+ ```typescript
223
+ // ✅ Correct: use offset for offset pagination, key for keyset
224
+ // Offset:
225
+ queryFn: (pagination) => ContactService.getAll({
226
+ offset: pagination.offset, // OffsetPaginationParams.offset
227
+ limit: pagination.limit,
228
+ })
229
+
230
+ // Keyset:
231
+ queryFn: (pagination) => ContactService.getAllKeyset({
232
+ key: pagination.key, // KeysetPaginationParams.key
233
+ limit: pagination.limit,
234
+ })
168
235
  ```
169
236
 
170
- Pagination requires the library to know which part of the response is the data array and which part is pagination metadata. Return an object with `data` (array) and `meta` (metadata).
237
+ `OffsetPaginationParams` has `{ offset: number, limit: number }`.
238
+ `KeysetPaginationParams` has `{ key?: any, limit: number }`.
171
239
 
172
- Source: `docs/packages/api-utils/pages/usage/paginated-query.md` Handling Pagination Results
240
+ Source: `src/types/pagination.type.ts`
173
241
 
174
242
  ### HIGH: Mix offset and keyset pagination patterns in same query
175
243
 
@@ -177,7 +245,7 @@ Source: `docs/packages/api-utils/pages/usage/paginated-query.md` Handling Pagina
177
245
  // ❌ Wrong: mixing pagination patterns
178
246
  const { result } = useOffsetInfiniteQuery('contactList', {
179
247
  queryFn: (pagination) => ContactService.getAllKeyset({
180
- cursor: pagination.pageParam, // offset expects page number!
248
+ key: pagination.key, // offset composable doesn't have key!
181
249
  limit: pagination.limit,
182
250
  }),
183
251
  })
@@ -185,26 +253,26 @@ const { result } = useOffsetInfiniteQuery('contactList', {
185
253
 
186
254
  ```typescript
187
255
  // ✅ Correct: match composable to backend API
188
- // Use useOffsetInfiniteQuery for page/limit APIs:
256
+ // Use useOffsetInfiniteQuery for offset/limit APIs:
189
257
  const { result } = useOffsetInfiniteQuery('contactList', {
190
258
  queryFn: (pagination) => ContactService.getAll({
191
- page: pagination.pageParam,
259
+ offset: pagination.offset,
192
260
  limit: pagination.limit,
193
261
  }),
194
262
  })
195
263
 
196
264
  // Use useKeysetInfiniteQuery for cursor-based APIs:
197
- const { result } = useKeysetInfiniteQuery('contactList', {
265
+ const { result } = useKeysetInfiniteQuery('contactListKeyset', {
198
266
  queryFn: (pagination) => ContactService.getAllKeyset({
199
- cursor: pagination.pageParam,
267
+ key: pagination.key,
200
268
  limit: pagination.limit,
201
269
  }),
202
270
  })
203
271
  ```
204
272
 
205
- Each composable expects a specific pagination parameter type. Offset expects a number; keyset expects a cursor string. Choose the right composable for your backend API.
273
+ Each composable expects a specific pagination parameter type. Choose the right composable for your backend API.
206
274
 
207
- Source: `docs/packages/api-utils/pages/usage/paginated-query.md` Offset vs Keyset comparison
275
+ Source: `src/composables/query/offsetInfiniteQuery.composable.ts` and `keysetInfiniteQuery.composable.ts`
208
276
 
209
277
  ### MEDIUM: Forget isFetchingNextPage flag; show loading on first page load
210
278
 
@@ -225,18 +293,16 @@ const { result, isFetchingNextPage, fetchNextPage } = useOffsetInfiniteQuery(...
225
293
  </button>
226
294
  ```
227
295
 
228
- `isFetching` is true during initial load and when fetching next pages. `isFetchingNextPage` is true only when loading additional pages. Use `isFetchingNextPage` for the load-more button.
296
+ `isFetching` is true during initial load and when fetching next pages. `isFetchingNextPage` is true only when loading additional pages.
229
297
 
230
- Source: `docs/packages/api-utils/pages/usage/paginated-query.md` Return Values
298
+ Source: `src/composables/query/offsetInfiniteQuery.composable.ts` `UseOffsetInfiniteQueryReturnType`
231
299
 
232
300
  ## Backend API Strategy
233
301
 
234
302
  > Offset vs keyset pagination depends entirely on your backend endpoint. Use the strategy your API provides.
235
- >
236
- > — Maintainer guidance
237
303
 
238
- If your API provides `page` and `limit` parameters, use `useOffsetInfiniteQuery`.
239
- If your API provides a `cursor` parameter, use `useKeysetInfiniteQuery`.
304
+ If your API accepts `offset` and `limit` parameters, use `useOffsetInfiniteQuery`.
305
+ If your API accepts a cursor `key` parameter, use `useKeysetInfiniteQuery`.
240
306
 
241
307
  ## See Also
242
308
 
@@ -1,14 +1,12 @@
1
1
  ---
2
2
  name: writing-mutations
3
3
  description: >
4
- Create, update, delete resources using factory-provided useMutation, typed queryKeysToInvalidate, AsyncResult error handling, execute function, request shape with body/params separation.
4
+ Create, update, delete resources using useMutation, typed queryKeysToInvalidate with optional param extractors, AsyncResult error handling, execute function, request shape with body/params separation.
5
5
  type: core
6
6
  library: vue-core-api-utils
7
- library_version: "0.0.3"
7
+ library_version: "1.2.0"
8
8
  sources:
9
- - "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/mutation.md"
10
9
  - "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/mutation/mutation.composable.ts"
11
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/factory/createApiMutationUtils.ts"
12
10
  ---
13
11
 
14
12
  # @wisemen/vue-core-api-utils — Writing Mutations
@@ -33,7 +31,7 @@ export function useCreateContact() {
33
31
  }
34
32
  ```
35
33
 
36
- Every mutation must list which queries to invalidate via `queryKeysToInvalidate`.
34
+ Every mutation should list which queries to invalidate via `queryKeysToInvalidate`.
37
35
 
38
36
  ## Core Patterns
39
37
 
@@ -52,8 +50,7 @@ async function handleSubmit(formData: ContactCreateForm) {
52
50
  // Invalidated queries will refetch automatically
53
51
  } else if (response.isErr()) {
54
52
  const error = response.getError()
55
- // Handle error based on code
56
- if (error.errors[0].code === 'EMAIL_EXISTS') {
53
+ if ('errors' in error && error.errors[0].code === 'EMAIL_EXISTS') {
57
54
  toast.error('That email is already registered')
58
55
  } else {
59
56
  toast.error('Creation failed')
@@ -64,29 +61,54 @@ async function handleSubmit(formData: ContactCreateForm) {
64
61
 
65
62
  Always `await execute()` and check the result state before continuing.
66
63
 
67
- ### Update mutation with specific query invalidation
64
+ ### Update mutation with specific query invalidation using param extractors
65
+
66
+ When you need to invalidate a specific query (rather than all queries with a key), pass param extractor functions:
68
67
 
69
68
  ```typescript
70
- export function useUpdateContact(contactUuid: string) {
71
- return useMutation({
72
- queryFn: async (options: { body: ContactUpdateForm }) => {
73
- return await ContactService.update(contactUuid, options.body)
69
+ export function useUpdateContact() {
70
+ return useMutation<ContactUpdateForm, Contact, { contactUuid: string }>({
71
+ queryFn: async (options) => {
72
+ return await ContactService.update(options.params.contactUuid, options.body)
73
+ },
74
+ queryKeysToInvalidate: {
75
+ // Invalidate only the specific contact that was updated
76
+ contactDetail: {
77
+ contactUuid: (params) => params.contactUuid,
78
+ },
79
+ // Invalidate all contact lists
80
+ contactList: {},
81
+ },
82
+ })
83
+ }
84
+ ```
85
+
86
+ Param extractors receive `(mutationParams, responseData)` and return the value for that query param. Empty object `{}` invalidates all queries with that key.
87
+
88
+ ### Mutation with URL params only (no body)
89
+
90
+ ```typescript
91
+ export function useDeleteContact() {
92
+ return useMutation<void, void, { contactUuid: string }>({
93
+ queryFn: async (options) => {
94
+ return await ContactService.delete(options.params.contactUuid)
74
95
  },
75
96
  queryKeysToInvalidate: {
76
- contactDetail: {}, // Invalidate the specific contact
77
- contactList: {}, // And the list
97
+ contactList: {},
78
98
  },
79
99
  })
80
100
  }
101
+
102
+ // execute({ params: { contactUuid: '123' } })
81
103
  ```
82
104
 
83
- You can invalidate multiple queries. Include queries that depend on the data you're changing.
105
+ When `TReqData` is `void`, the `execute` call takes `{ params: TParams }` instead of `{ body, params }`.
84
106
 
85
107
  ### Form integration
86
108
 
87
109
  ```vue
88
110
  <script setup lang="ts">
89
- import { ref } from 'vue'
111
+ import { reactive } from 'vue'
90
112
  import { useCreateContact } from '@/composables'
91
113
 
92
114
  const form = reactive({ name: '', email: '' })
@@ -110,7 +132,7 @@ async function handleSubmit() {
110
132
  {{ result.isLoading() ? 'Creating...' : 'Create' }}
111
133
  </button>
112
134
  <div v-if="result.isErr()">
113
- Error: {{ result.getError().errors[0].detail }}
135
+ Error: {{ result.getError().errors?.[0]?.detail }}
114
136
  </div>
115
137
  </form>
116
138
  </template>
@@ -120,7 +142,7 @@ Use `result.isLoading()` to disable the button during mutation.
120
142
 
121
143
  ## Common Mistakes
122
144
 
123
- ### CRITICAL: Import useMutation from @tanstack/vue-query instead of factory
145
+ ### CRITICAL: Import useMutation from @tanstack/vue-query instead of your api module
124
146
 
125
147
  ```typescript
126
148
  // ❌ Wrong: using TanStack directly
@@ -136,7 +158,7 @@ const mutation = useMutation({
136
158
  ```
137
159
 
138
160
  ```typescript
139
- // ✅ Correct: use factory composable
161
+ // ✅ Correct: use the composable from your api module
140
162
  import { useMutation } from '@/api'
141
163
 
142
164
  const { execute, result } = useMutation({
@@ -150,9 +172,9 @@ const { execute, result } = useMutation({
150
172
  // Full AsyncResult, type-safe queryKeysToInvalidate, error codes
151
173
  ```
152
174
 
153
- Direct TanStack import loses the factory's type safety and AsyncResult wrapping.
175
+ Direct TanStack import loses type safety and AsyncResult wrapping.
154
176
 
155
- Source: Library architecture — always use composables from `createApiUtils()` factory
177
+ Source: `src/composables/mutation/mutation.composable.ts`
156
178
 
157
179
  ### CRITICAL: Forget to list queryKeysToInvalidate; cache becomes stale
158
180
 
@@ -180,9 +202,9 @@ const { execute } = useMutation({
180
202
  // After success, contactList queries refetch
181
203
  ```
182
204
 
183
- If you don't list which queries to invalidate, the cache stays stale and the UI shows outdated data. Update returns success but list still shows old items.
205
+ If you don't list which queries to invalidate, the cache stays stale and the UI shows outdated data.
184
206
 
185
- Source: `docs/packages/api-utils/pages/usage/mutation.md` Create Mutation Example
207
+ Source: `src/composables/mutation/mutation.composable.ts` `onSuccess` invalidation logic
186
208
 
187
209
  ### HIGH: Not await execute(); code runs before mutation completes
188
210
 
@@ -205,34 +227,37 @@ async function handleSubmit() {
205
227
  }
206
228
  ```
207
229
 
208
- Not awaiting `execute()` means the mutation is still in flight when you navigate away or access the result. Always await and check the result before continuing.
230
+ Not awaiting `execute()` means the mutation is still in flight when you navigate away or access the result.
209
231
 
210
- Source: `docs/packages/api-utils/pages/usage/mutation.md` Usage in Component
232
+ Source: `src/composables/mutation/mutation.composable.ts` `execute` returns `Promise<ApiResult>`
211
233
 
212
- ### HIGH: Use body instead of params for query parameters
234
+ ### HIGH: Use body instead of params for URL parameters
213
235
 
214
236
  ```typescript
215
- // ❌ Wrong: filter/search as body
216
- const { execute } = useMutation({
237
+ // ❌ Wrong: URL params passed as body
238
+ const { execute } = useMutation<SearchForm, Results, void>({
217
239
  queryFn: async (options) => {
218
- return await SearchService.search(options.body) // params should go in URL!
240
+ return await SearchService.search(options.body) // URL params shouldn't be in body
219
241
  },
220
242
  })
221
243
  ```
222
244
 
223
245
  ```typescript
224
- // ✅ Correct: separate body and params
225
- const { execute } = useMutation({
246
+ // ✅ Correct: separate body (payload) from params (URL query string)
247
+ const { execute } = useMutation<SearchForm, Results, { category: string }>({
226
248
  queryFn: async (options) => {
227
249
  const { body, params } = options
228
- return await SearchService.search(params) // URL params
250
+ return await SearchService.search(body, params.category)
229
251
  },
230
252
  })
253
+
254
+ // Call with both:
255
+ execute({ body: searchForm, params: { category: 'contacts' } })
231
256
  ```
232
257
 
233
- When mutations accept query parameters (filters, search), pass them in the `params` field of the options, not `body`. `body` is for request payload; `params` is for URL query string.
258
+ `body` is for the request payload (POST/PUT body); `params` is for URL query string parameters. The `RequestParams` type enforces this shape automatically based on your generics.
234
259
 
235
- Source: Source code `mutation.composable.ts` — request shape documentation
260
+ Source: `src/composables/mutation/mutation.composable.ts` — `RequestParams` type
236
261
 
237
262
  ## See Also
238
263
 
@@ -1,13 +1,11 @@
1
1
  ---
2
2
  name: writing-queries
3
3
  description: >
4
- Single resource queries using factory-provided useQuery, computed ref params, staleTime configuration, queryFn, refetch, isFetching vs isLoading distinctions, automatic cache management.
4
+ Single resource queries using useQuery, computed ref params, staleTime configuration, queryFn, refetch, isFetching vs isLoading distinctions, automatic cache management.
5
5
  type: core
6
6
  library: vue-core-api-utils
7
- library_version: "0.0.3"
7
+ library_version: "1.2.0"
8
8
  sources:
9
- - "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/query.md"
10
- - "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/overview.md"
11
9
  - "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/query.composable.ts"
12
10
  ---
13
11
 
@@ -73,6 +71,20 @@ const { result } = useQuery('contactDetail', {
73
71
 
74
72
  `staleTime` determines how long cached data is considered fresh. After this time, the next query interaction triggers a background refetch.
75
73
 
74
+ ### Conditionally enable a query
75
+
76
+ ```typescript
77
+ const isEnabled = computed(() => contactUuid.value !== null)
78
+
79
+ const { result } = useQuery('contactDetail', {
80
+ params: { contactUuid: computed(() => contactUuid.value!) },
81
+ queryFn: () => ContactService.getByUuid(contactUuid.value!),
82
+ isEnabled,
83
+ })
84
+ ```
85
+
86
+ Use `isEnabled` to prevent the query from running until required data is available.
87
+
76
88
  ### Manually refetch on demand
77
89
 
78
90
  ```typescript
@@ -92,7 +104,7 @@ if (result.value.isOk()) {
92
104
 
93
105
  ## Common Mistakes
94
106
 
95
- ### CRITICAL: Import useQuery from @tanstack/vue-query instead of factory
107
+ ### CRITICAL: Import useQuery from @tanstack/vue-query instead of your api module
96
108
 
97
109
  ```typescript
98
110
  // ❌ Wrong: using TanStack directly
@@ -106,7 +118,7 @@ const { data, error, isLoading } = useQuery({
106
118
  ```
107
119
 
108
120
  ```typescript
109
- // ✅ Correct: use factory-provided composable
121
+ // ✅ Correct: use the composable from your api module (or directly from the library)
110
122
  import { useQuery } from '@/api'
111
123
  import { computed } from 'vue'
112
124
 
@@ -118,9 +130,9 @@ const { result, isLoading } = useQuery('contactDetail', {
118
130
  // Full type safety, AsyncResult wrapping, automatic error codes
119
131
  ```
120
132
 
121
- Importing directly from @tanstack/vue-query bypasses the typed factory, losing AsyncResult wrapping, type-safe query keys, and error code typing.
133
+ Importing directly from @tanstack/vue-query bypasses the typed composable, losing AsyncResult wrapping, type-safe query keys, and error code typing.
122
134
 
123
- Source: Library architecture — always use composables from `createApiUtils()` factory
135
+ Source: `src/composables/query/query.composable.ts`
124
136
 
125
137
  ### HIGH: Use plain ref for params instead of computed
126
138
 
@@ -147,9 +159,9 @@ const { result } = useQuery('userDetail', {
147
159
 
148
160
  When params are plain refs, the query doesn't watch them and the cache isn't invalidated when the param changes.
149
161
 
150
- Source: `docs/packages/api-utils/pages/usage/query.md` Usage in Vue Component section
162
+ Source: `src/composables/query/query.composable.ts` `NestedMaybeRefOrGetter` type
151
163
 
152
- ### HIGH: Not set staleTime; serve stale cache indefinitely
164
+ ### HIGH: Not set staleTime; background refetch on every interaction
153
165
 
154
166
  ```typescript
155
167
  // ❌ Wrong: no staleTime; background refetch constantly
@@ -173,8 +185,6 @@ const { result } = useQuery('userDetail', {
173
185
 
174
186
  Default `staleTime` is 0, meaning the cache is immediately considered stale. Every interaction triggers a background refetch. Set `staleTime` based on how frequently the data changes.
175
187
 
176
- Source: `docs/packages/api-utils/pages/getting-started/installation.md` Setup section
177
-
178
188
  ### MEDIUM: Confuse isFetching with isLoading
179
189
 
180
190
  ```typescript
@@ -195,9 +205,11 @@ if (result.value.isLoading()) {
195
205
  // Use isFetching separately for background fetch indicator
196
206
  ```
197
207
 
198
- `isLoading` is true only during the initial fetch. `isFetching` is true whenever any fetch is in progress (including background refetches). Use `result.isLoading()` for conditional rendering; use `isFetching` for loading indicators on load-more buttons.
208
+ `isLoading` is true only during the initial fetch. `isFetching` is true whenever any fetch is in progress (including background refetches). Use `result.isLoading()` for conditional rendering; use `isFetching` for loading indicators.
209
+
210
+ Note: `isLoading`, `isError`, and `isSuccess` on the return type are deprecated — prefer `result.value.isLoading()`, `result.value.isErr()`, and `result.value.isOk()`.
199
211
 
200
- Source: `docs/packages/api-utils/pages/usage/query.md` Return Values section
212
+ Source: `src/composables/query/query.composable.ts` `UseQueryReturnType`
201
213
 
202
214
  ## See Also
203
215