@wisemen/vue-core-api-utils 1.2.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.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "type": "module",
7
- "version": "1.2.0",
7
+ "version": "2.0.0",
8
8
  "license": "SEE LICENSE IN LICENSE.md",
9
9
  "repository": {
10
10
  "type": "git",
@@ -45,7 +45,7 @@
45
45
  "typescript": "5.9.3",
46
46
  "vue": "3.5.27",
47
47
  "vitest": "4.1.0",
48
- "@wisemen/eslint-config-vue": "2.1.2"
48
+ "@wisemen/eslint-config-vue": "2.1.3"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "tsdown",
@@ -4,10 +4,8 @@ description: >
4
4
  Three-state AsyncResult type (Loading, Ok, Err), isLoading/isOk/isErr type predicates, getValue/getError accessors, match() pattern matching, map/mapErr transformations, safe value extraction without undefined.
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/concepts/result-types.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/async-result/asyncResult.ts"
12
10
  ---
13
11
 
@@ -149,15 +147,15 @@ If you omit a handler, TypeScript errors and the UI renders nothing during the o
149
147
 
150
148
  Source: `docs/packages/api-utils/pages/concepts/result-types.md` Pattern Matching Section
151
149
 
152
- ### HIGH: Use state flags (isLoading, isError, isSuccess) instead of AsyncResult state
150
+ ### HIGH: Use deprecated state flags (isLoading, isError, isSuccess) instead of AsyncResult state
153
151
 
154
152
  ```typescript
155
- // ❌ Wrong: mixing old flags with AsyncResult
153
+ // ❌ Wrong: using deprecated flags
156
154
  const { result, isLoading } = useQuery(...)
157
155
  if (isLoading.value) {
158
156
  // Show spinner
159
157
  } else {
160
- const data = result.value.getValue() // Could be null!
158
+ const data = result.value.getValue() // Could be null if isErr!
161
159
  }
162
160
  ```
163
161
 
@@ -171,9 +169,9 @@ if (result.value.isLoading()) {
171
169
  }
172
170
  ```
173
171
 
174
- Composables export both AsyncResult (exhaustive) and backward-compatible flags (`isLoading`, `isError`, `isSuccess`). Mixing them causes logic bugs where flags say the query is done but the result is still loading.
172
+ `isLoading`, `isError`, and `isSuccess` on `UseQueryReturnType` are deprecated they exist for backward compatibility but are less type-safe than AsyncResult. Always prefer `result.value.isLoading()`, `result.value.isErr()`, and `result.value.isOk()`.
175
173
 
176
- Source: Maintainer interview library provides both patterns for backward compatibility, but agents should prefer AsyncResult
174
+ Source: `src/composables/query/query.composable.ts``UseQueryReturnType` deprecated annotations
177
175
 
178
176
  ## Next Steps
179
177
 
@@ -1,29 +1,43 @@
1
1
  ---
2
2
  name: cache-management
3
3
  description: >
4
- Type-safe QueryClient with get/set/update/invalidate methods, predicate-based updates, cascade invalidation strategy, shared cache across components, lazy refetch patterns.
4
+ Type-safe QueryClient class with get/set/update/invalidate methods, rollback support from update(), predicate-based updates, cascade invalidation strategy, shared cache across components.
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-client.md"
10
9
  - "wisemen-digital/wisemen-core:packages/web/api-utils/src/utils/query-client/queryClient.ts"
11
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/factory/createApiQueryClientUtils.ts"
10
+ - "wisemen-digital/wisemen-core:packages/web/api-utils/src/config/config.ts"
12
11
  ---
13
12
 
14
13
  # @wisemen/vue-core-api-utils — Cache Management
15
14
 
16
- Manually read, write, update, and invalidate the query cache using the type-safe `useQueryClient()` composable. This is useful for optimistic updates and strategically invalidating affected queries.
15
+ Manually read, write, update, and invalidate the query cache using the type-safe `QueryClient` class. This is useful for optimistic updates and strategically invalidating affected queries.
17
16
 
18
17
  ## Setup
19
18
 
19
+ The library exports a `QueryClient` class that wraps the underlying TanStack QueryClient with type-safe methods. Create a helper function in your project to get a typed instance:
20
+
21
+ ```typescript
22
+ // src/api/queryClient.ts
23
+
24
+ import { QueryClient, getTanstackQueryClient } from '@wisemen/vue-core-api-utils'
25
+ import type { ProjectQueryKeys } from '@/types/queryKey.type'
26
+
27
+ export function useQueryClient() {
28
+ return new QueryClient<ProjectQueryKeys>(getTanstackQueryClient())
29
+ }
30
+ ```
31
+
32
+ Then use it anywhere in your composables or components:
33
+
20
34
  ```typescript
21
- import { useQueryClient } from '@/api'
35
+ import { useQueryClient } from '@/api/queryClient'
22
36
 
23
37
  const queryClient = useQueryClient()
24
38
 
25
39
  // Get cached data
26
- const contact = queryClient.get(['contactDetail', { contactUuid: '123' }])
40
+ const contacts = queryClient.get('contactDetail')
27
41
 
28
42
  // Set cached data
29
43
  queryClient.set(
@@ -31,14 +45,14 @@ queryClient.set(
31
45
  updatedContact
32
46
  )
33
47
 
34
- // Update cached data with a predicate
35
- queryClient.update('contactList', {
48
+ // Update cached data with a predicate (returns rollback function)
49
+ const { rollback } = queryClient.update('contactList', {
36
50
  by: (contact) => contact.id === '123',
37
51
  value: (contact) => ({ ...contact, name: 'Updated' }),
38
52
  })
39
53
 
40
54
  // Invalidate queries (trigger refetch)
41
- queryClient.invalidate('contactList')
55
+ await queryClient.invalidate('contactList')
42
56
  ```
43
57
 
44
58
  ## Core Patterns
@@ -48,46 +62,42 @@ queryClient.invalidate('contactList')
48
62
  ```typescript
49
63
  const queryClient = useQueryClient()
50
64
 
51
- // Get specific query
52
- const contact = queryClient.get(
53
- ['contactDetail', { contactUuid: '123' }]
54
- )
65
+ // Get all queries with a key (returns array)
66
+ const allContacts = queryClient.get('contactDetail')
55
67
 
56
- // Get all queries with a key
57
- const allContacts = queryClient.get('contactList')
68
+ // Get exact query stored as ['contactDetail']
69
+ const exactContact = queryClient.get('contactDetail', { isExact: true })
58
70
 
59
- // Get exact query only
60
- const specificQuery = queryClient.get('contactList', { isExact: true })
71
+ // Get specific query with params (returns single item or null)
72
+ const contact = queryClient.get(['contactDetail', { contactUuid: '123' }] as const)
61
73
  ```
62
74
 
63
- Returns the cached data or null if not cached. The QueryClient infers entity type from your query key definition.
75
+ Returns the cached entity or null if not cached. The QueryClient infers the entity type from your query key definition.
64
76
 
65
77
  ### Set cached data
66
78
 
67
79
  ```typescript
68
80
  const queryClient = useQueryClient()
69
81
 
82
+ // Set query with key + params
70
83
  queryClient.set(
71
84
  ['contactDetail', { contactUuid: '123' }],
72
- { id: '123', name: 'John', email: 'john@email.com' }
85
+ { uuid: '123', name: 'John', email: 'john@email.com' }
73
86
  )
74
87
 
75
- // For lists, set works with arrays too
76
- queryClient.set('contactList', [
77
- { id: '123', name: 'John' },
78
- { id: '456', name: 'Jane' },
79
- ])
88
+ // Set query with just the key (stores as ['contactDetail'])
89
+ queryClient.set('contactDetail', { uuid: '123', name: 'John', email: 'john@email.com' })
80
90
  ```
81
91
 
82
- `set()` replaces all cached data for that query key.
92
+ `set()` replaces all cached data for that specific query key.
83
93
 
84
94
  ### Update cached data with predicates
85
95
 
86
96
  ```typescript
87
97
  const queryClient = useQueryClient()
88
98
 
89
- // Update a single item in a list
90
- queryClient.update('contactList', {
99
+ // Update a single item in a list — returns { rollback } for reverting
100
+ const { rollback } = queryClient.update('contactList', {
91
101
  by: (contact) => contact.id === '123', // Predicate
92
102
  value: (contact) => ({ // Transform
93
103
  ...contact,
@@ -95,14 +105,25 @@ queryClient.update('contactList', {
95
105
  }),
96
106
  })
97
107
 
98
- // For single entities, the predicate always matches
99
- queryClient.update('contactDetail', {
100
- by: (contact) => true, // Always update
108
+ // If the operation should be reverted (e.g. mutation failed):
109
+ rollback()
110
+ ```
111
+
112
+ `update()` returns a `{ rollback }` function that reverts the cache to its state before the update. Safe to call multiple times (subsequent calls are no-ops).
113
+
114
+ ### Update specific query with params tuple
115
+
116
+ ```typescript
117
+ const queryClient = useQueryClient()
118
+
119
+ // Update only the specific cached query for this contact
120
+ queryClient.update(['contactDetail', { contactUuid: '123' }] as const, {
121
+ by: () => true, // Single entity — always matches
101
122
  value: (contact) => ({ ...contact, name: 'Updated' }),
102
123
  })
103
124
  ```
104
125
 
105
- QueryClient knows whether the entity is an array or single item, so predicates work transparently on lists.
126
+ Using a key+params tuple updates only the specific query, not all queries with that key.
106
127
 
107
128
  ### Invalidate and refetch
108
129
 
@@ -110,10 +131,10 @@ QueryClient knows whether the entity is an array or single item, so predicates w
110
131
  const queryClient = useQueryClient()
111
132
 
112
133
  // Invalidate all queries with this key
113
- queryClient.invalidate('contactList')
134
+ await queryClient.invalidate('contactList')
114
135
 
115
- // Invalidate specific query
116
- queryClient.invalidate(['contactDetail', { contactUuid: '123' }])
136
+ // Invalidate specific query with params
137
+ await queryClient.invalidate(['contactDetail', { contactUuid: '123' }] as const)
117
138
 
118
139
  // After invalidation, the next query interaction triggers a refetch
119
140
  ```
@@ -122,80 +143,95 @@ Invalidation marks cached data as stale. The next interaction (component mount,
122
143
 
123
144
  ## Common Mistakes
124
145
 
125
- ### HIGH: Call update/set without checking data structure (array vs entity)
146
+ ### HIGH: Create QueryClient without typed query keys; lose type safety
126
147
 
127
148
  ```typescript
128
- // ❌ Wrong: treating array like entity
129
- const queryClient = useQueryClient()
149
+ // ❌ Wrong: untyped QueryClient
150
+ import { QueryClient, getTanstackQueryClient } from '@wisemen/vue-core-api-utils'
151
+
152
+ const queryClient = new QueryClient(getTanstackQueryClient())
153
+ // All methods fall back to `object` for query keys — no autocomplete, no type checking
154
+ ```
155
+
156
+ ```typescript
157
+ // ✅ Correct: typed QueryClient
158
+ import { QueryClient, getTanstackQueryClient } from '@wisemen/vue-core-api-utils'
159
+ import type { ProjectQueryKeys } from '@/types/queryKey.type'
160
+
161
+ const queryClient = new QueryClient<ProjectQueryKeys>(getTanstackQueryClient())
162
+ // queryClient.get('nonExistentKey') → TypeScript error
163
+ // queryClient.update('contactList', { by: (c) => c.nonExistentField }) → TypeScript error
164
+ ```
165
+
166
+ Always provide your `ProjectQueryKeys` type generic when instantiating QueryClient.
167
+
168
+ Source: `src/utils/query-client/queryClient.ts` — `QueryClient<TQueryKeys>` class
169
+
170
+ ### HIGH: Discard the rollback return from update(); can't revert optimistic changes
171
+
172
+ ```typescript
173
+ // ❌ Wrong: ignoring rollback
130
174
  queryClient.update('contactList', {
131
- by: (contact) => contact.id === '123',
132
- value: (contact) => ({ ...contact, name: 'Updated' }),
175
+ by: (c) => c.id === '123',
176
+ value: (c) => ({ ...c, name: 'Updated' }),
133
177
  })
134
- // If contactList is Contact[], predicate matches each item individually
135
- // If you expect single match, this breaks silently
178
+ // No way to undo if the mutation fails
136
179
  ```
137
180
 
138
181
  ```typescript
139
- // ✅ Correct: QueryClient infers type from query key
140
- const queryClient = useQueryClient()
141
- // If contactList: { entity: Contact[], ... }
142
- // QueryClient knows to iterate the array and apply predicate to each item
143
- queryClient.update('contactList', {
144
- by: (contact) => contact.id === '123',
145
- value: (contact) => ({ ...contact, name: 'Updated' }),
182
+ // ✅ Correct: capture rollback for error recovery
183
+ const { rollback } = queryClient.update('contactList', {
184
+ by: (c) => c.id === '123',
185
+ value: (c) => ({ ...c, name: 'Updated' }),
146
186
  })
147
- // For single entities: { entity: Contact, ... }
148
- // QueryClient provides the entity directly to predicate
187
+
188
+ const result = await execute(formData)
189
+ if (result.isErr()) {
190
+ rollback() // Reverts the cache to its pre-update state
191
+ }
149
192
  ```
150
193
 
151
- QueryClient infers data structure from your query key definition, so the same `update()` call works correctly for arrays and single entities. The type system ensures you're using the right predicate signature.
194
+ `update()` returns `{ rollback }` always capture it when doing optimistic updates so you can revert on error.
152
195
 
153
- Source: `docs/packages/api-utils/pages/usage/query-client.md` Usage
196
+ Source: `src/utils/query-client/queryClient.ts` — `QueryClientUpdateResult`
154
197
 
155
- ### MEDIUM: Call set() without async loading state; UI flashes stale data
198
+ ### MEDIUM: Call set() without rollback plan; UI flashes stale data on error
156
199
 
157
200
  ```typescript
158
- // ❌ Wrong: immediate set without loading indicator
159
- const queryClient = useQueryClient()
160
- queryClient.set(['contactDetail', { contactUuid }], updatedData)
161
- // Cache updated but no indicator that request is pending
162
- // UI looks responsive but actually has unconfirmed data
201
+ // ❌ Wrong: immediate set without error recovery
202
+ queryClient.set(['contactDetail', { contactUuid }], newData)
203
+ // Cache updated but if the mutation fails, data is wrong with no way to revert
163
204
  ```
164
205
 
165
206
  ```typescript
166
- // ✅ Correct: pair optimistic update with mutation result handling
167
- const queryClient = useQueryClient()
168
- const original = queryClient.get(['contactDetail', { contactUuid }])
169
-
170
- // Update cache optimistically
171
- queryClient.set(['contactDetail', { contactUuid }], newData)
207
+ // ✅ Correct: prefer update() which has built-in rollback
208
+ const { rollback } = queryClient.update(['contactDetail', { contactUuid }] as const, {
209
+ by: () => true,
210
+ value: () => newData,
211
+ })
172
212
 
173
- // Execute mutation
174
213
  const result = await execute(formData)
175
214
  if (result.isErr()) {
176
- // Rollback on error
177
- queryClient.set(['contactDetail', { contactUuid }], original)
215
+ rollback()
178
216
  }
179
217
  ```
180
218
 
181
- If you update the cache without a mutation in flight, the UI looks responsive but the data is unconfirmed. Always pair cache updates with mutation execution and rollback on error.
219
+ Prefer `update()` over `set()` for optimistic changes because `update()` automatically captures the previous state for rollback.
182
220
 
183
- Source: `docs/packages/api-utils/pages/usage/query-client.md` Real-World Example
221
+ Source: `src/utils/query-client/queryClient.ts`
184
222
 
185
223
  ## Cache Strategy
186
224
 
187
225
  > Explicitly invalidate only the queries affected by the mutation. Let lazy refetch handle the rest when users navigate to pages needing other data.
188
- >
189
- > — Maintainer guidance
190
226
 
191
227
  When a mutation succeeds, look at what changed:
192
228
  - If you updated a contact, invalidate `contactDetail` and `contactList` (they both show that contact)
193
- - If you archived a conversation, invalidate `conversationList` (but maybe not `conversationDetail` unless showing the one you archived)
229
+ - If you archived a conversation, invalidate `conversationList` (but maybe not `conversationDetail` unless it's the one you archived)
194
230
  - Don't invalidate unrelated queries — let them refetch lazily when needed
195
231
 
196
232
  ## Shared Cache Across Components
197
233
 
198
- Important: Multiple components using the same query key share the same cached data. This is a feature, not a bug.
234
+ Multiple components using the same query key share the same cached data. This is a feature, not a bug.
199
235
 
200
236
  ```typescript
201
237
  // ComponentA
@@ -210,8 +246,8 @@ const { result: resultB } = useQuery('userDetail', {
210
246
  queryFn: () => UserService.getById('same-id'),
211
247
  })
212
248
 
213
- // resultA and resultB are the SAME cached value
214
- // Mutation in B invalidates A's cache
249
+ // resultA and resultB share the SAME cached value
250
+ // Mutation in B that invalidates userDetail also triggers a refetch in A
215
251
  ```
216
252
 
217
253
  Use this to your advantage: invalidate a query and all components using it refetch automatically.
@@ -220,3 +256,4 @@ Use this to your advantage: invalidate a query and all components using it refet
220
256
 
221
257
  - [Writing Mutations](../writing-mutations/SKILL.md) — Every mutation needs to know which queries to invalidate
222
258
  - [Writing Queries](../writing-queries/SKILL.md) — Understanding caching strategy informs cache management choices
259
+ - [Optimistic UIs](../optimistic-uis/SKILL.md) — Full optimistic update pattern using update() and rollback
@@ -4,9 +4,8 @@ description: >
4
4
  neverthrow Result architectural basis; three-state AsyncResult relationship to Result; @tanstack/vue-query lifecycle (staleTime, gcTime, refetch); composition of TanStack Query + neverthrow + Vue 3 reactivity.
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/concepts/result-types.md"
10
9
  - "wisemen-digital/wisemen-core:packages/web/api-utils/src/async-result/asyncResult.ts"
11
10
  - "wisemen-digital/wisemen-core:packages/web/api-utils/src/types/apiError.type.ts"
12
11
  - "wisemen-digital/wisemen-core:packages/web/api-utils/src/config/config.ts"
@@ -1,19 +1,19 @@
1
1
  ---
2
2
  name: getting-started
3
3
  description: >
4
- Install @wisemen/vue-core-api-utils, initialize apiUtilsPlugin with QueryClient config, define typed query keys interface, create API composables with error codes.
4
+ Install @wisemen/vue-core-api-utils, initialize apiUtilsPlugin with QueryClient config, register typed query keys via module augmentation, re-export composables.
5
5
  type: lifecycle
6
6
  library: vue-core-api-utils
7
- library_version: "1.0.1"
7
+ library_version: "1.2.0"
8
8
  sources:
9
- - "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/getting-started/installation.md"
10
9
  - "wisemen-digital/wisemen-core:packages/web/api-utils/src/plugin/apiUtilsPlugin.ts"
10
+ - "wisemen-digital/wisemen-core:packages/web/api-utils/src/register.ts"
11
11
  - "wisemen-digital/wisemen-core:packages/web/api-utils/src/config/config.ts"
12
12
  ---
13
13
 
14
14
  # @wisemen/vue-core-api-utils — Getting Started
15
15
 
16
- Get `@wisemen/vue-core-api-utils` installed, your Vue Query plugin initialized, query keys defined, and typed composables created.
16
+ Get `@wisemen/vue-core-api-utils` installed, your Vue Query plugin initialized, query keys defined, and typed composables available.
17
17
 
18
18
  ## Setup
19
19
 
@@ -40,13 +40,13 @@ export interface ProjectQueryKeys {
40
40
  // List query with offset pagination
41
41
  contactList: {
42
42
  entity: Contact[]
43
- params: { page: number; limit: number; search?: string }
43
+ params: { search?: string }
44
44
  }
45
45
 
46
46
  // List query with keyset pagination
47
47
  contactListKeyset: {
48
48
  entity: Contact[]
49
- params: { limit: number; key?: string }
49
+ params: { search?: string }
50
50
  }
51
51
  }
52
52
  ```
@@ -78,7 +78,9 @@ app.mount('#app')
78
78
 
79
79
  The `apiUtilsPlugin` function creates a QueryClient with your config and handles @tanstack/vue-query setup internally.
80
80
 
81
- ### 4. Create your API composables
81
+ ### 4. Register your types and re-export composables
82
+
83
+ Use module augmentation to register your query keys and error codes for library-wide type safety, then re-export composables for convenient importing across your project:
82
84
 
83
85
  ```typescript
84
86
  // src/api/index.ts
@@ -88,15 +90,23 @@ import type {
88
90
  KeysetPaginationResult as ApiUtilsKeysetPaginationResult,
89
91
  OffsetPaginationResult as ApiUtilsOffsetPaginationResult,
90
92
  } from '@wisemen/vue-core-api-utils'
91
- import { createApiUtils } from '@wisemen/vue-core-api-utils'
92
93
 
93
94
  import type { ProjectQueryKeys } from '@/types/queryKey.type'
94
95
 
95
96
  // Define your error codes
96
97
  export type ERROR_KEYS = 'NOT_FOUND' | 'UNAUTHORIZED' | 'NETWORK_ERROR' | 'VALIDATION_ERROR'
97
98
 
98
- // Create factory with your types
99
- export const {
99
+ // Register query keys and error codes via module augmentation
100
+ // This makes all composables fully typed without a factory function
101
+ declare module '@wisemen/vue-core-api-utils' {
102
+ interface Register {
103
+ queryKeys: ProjectQueryKeys
104
+ errorCodes: ERROR_KEYS
105
+ }
106
+ }
107
+
108
+ // Re-export composables for convenient importing
109
+ export {
100
110
  useKeysetInfiniteQuery,
101
111
  useMutation,
102
112
  useOffsetInfiniteQuery,
@@ -104,8 +114,7 @@ export const {
104
114
  usePrefetchKeysetInfiniteQuery,
105
115
  usePrefetchOffsetInfiniteQuery,
106
116
  usePrefetchQuery,
107
- useQueryClient,
108
- } = createApiUtils<ProjectQueryKeys, ERROR_KEYS>()
117
+ } from '@wisemen/vue-core-api-utils'
109
118
 
110
119
  // Export typed result types
111
120
  export type ApiResult<T> = ApiUtilsApiResult<T, ERROR_KEYS>
@@ -113,6 +122,8 @@ export type OffsetPaginationResult<T> = ApiUtilsOffsetPaginationResult<T, ERROR_
113
122
  export type KeysetPaginationResult<T> = ApiUtilsKeysetPaginationResult<T, ERROR_KEYS>
114
123
  ```
115
124
 
125
+ The `declare module` block tells the library about your project's types. After this, every composable automatically knows your query keys and error codes.
126
+
116
127
  ## Core Patterns
117
128
 
118
129
  ### Create a detail query composable
@@ -185,7 +196,7 @@ All queries and mutations return `AsyncResult` with three states: loading, ok, a
185
196
 
186
197
  ## Common Mistakes
187
198
 
188
- ### CRITICAL: Forget to initialize apiUtilsPlugin with QueryClient config
199
+ ### CRITICAL: Forget to initialize apiUtilsPlugin
189
200
 
190
201
  ```typescript
191
202
  // ❌ Wrong: plugin not initialized
@@ -205,7 +216,7 @@ app.use(apiUtilsPlugin({
205
216
  app.mount('#app')
206
217
  ```
207
218
 
208
- If you skip the plugin, `createApiUtils()` has no QueryClient and throws immediately.
219
+ Without the plugin, composables have no QueryClient and throw immediately.
209
220
 
210
221
  Source: `src/config/config.ts` — `getQueryClient()` assertion
211
222
 
@@ -228,21 +239,46 @@ export interface ProjectQueryKeys {
228
239
  }
229
240
  contactList: {
230
241
  entity: Contact[]
231
- params: { page: number; limit: number }
242
+ params: { search?: string }
243
+ }
244
+ }
245
+ ```
246
+
247
+ Query keys without proper structure cause TypeScript errors and prevent correct query key resolution.
248
+
249
+ Source: `src/register.ts` — `RegisteredQueryKeyEntity` and `RegisteredQueryKeyParams` type derivation
250
+
251
+ ### HIGH: Skip the module augmentation; composables lose type safety
252
+
253
+ ```typescript
254
+ // ❌ Wrong: no declare module block in @/api/index.ts
255
+ // Composables fall back to `object` for query keys and `string` for error codes
256
+ // TypeScript won't catch wrong query key names or invalid error codes
257
+ ```
258
+
259
+ ```typescript
260
+ // ✅ Correct: declare module augments the Register interface
261
+ declare module '@wisemen/vue-core-api-utils' {
262
+ interface Register {
263
+ queryKeys: ProjectQueryKeys
264
+ errorCodes: ERROR_KEYS
232
265
  }
233
266
  }
267
+ // Now useQuery('nonExistentKey', ...) is a compile error
268
+ // And error.code is typed as ERROR_KEYS, not just string
234
269
  ```
235
270
 
236
- Query keys without proper structure prevent the factory from typing composables correctly and cause runtime errors during query key resolution.
271
+ Without the `declare module` block the library types fall back to generic `object` and `string`. All query key checks and error code checks become useless at compile time.
237
272
 
238
- Source: `docs/packages/api-utils/pages/getting-started/installation.md` Section 3
273
+ Source: `src/register.ts`
239
274
 
240
275
  ## You're all set!
241
276
 
242
277
  You now have:
243
278
  - ✅ Plugin initialized with Vue Query
244
279
  - ✅ Query keys defined with types
245
- - ✅ API composables created
280
+ - ✅ Types registered via module augmentation
281
+ - ✅ Composables re-exported from `@/api`
246
282
  - ✅ Error codes enumerated
247
283
 
248
- Head to [writing-queries](../writing-queries/SKILL.md) to fetch your first resource, or [handling-asyncresult-types](../asyncresult-handling/SKILL.md) to understand the three-state AsyncResult type.
284
+ Head to [writing-queries](../writing-queries/SKILL.md) to fetch your first resource, or [asyncresult-handling](../asyncresult-handling/SKILL.md) to understand the three-state AsyncResult type.