auto-api-hooks 1.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/LICENSE +21 -0
- package/README.md +1039 -0
- package/dist/cli.js +3308 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +3233 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +297 -0
- package/dist/index.d.ts +297 -0
- package/dist/index.js +3218 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
package/README.md
ADDED
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
# auto-api-hooks
|
|
2
|
+
|
|
3
|
+
Auto-generate type-safe React hooks from API specifications.
|
|
4
|
+
|
|
5
|
+
`auto-api-hooks` reads your OpenAPI 3.x, Swagger 2.0, or GraphQL schema and produces ready-to-use React hooks, TypeScript types, optional Zod validation schemas, and MSW v2 mock handlers -- all from a single command.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Multi-spec support** -- Generate React hooks from OpenAPI 3.x, Swagger 2.0, and GraphQL schemas
|
|
10
|
+
- **Multiple fetcher strategies** -- Plain `fetch`, Axios, TanStack React Query v5, and SWR
|
|
11
|
+
- **Optional Zod validation** -- Generate Zod schemas with format-aware refinements for runtime response validation
|
|
12
|
+
- **Smart pagination detection** -- Automatically detects cursor-based, offset-limit, and page-number pagination patterns and generates infinite query hooks
|
|
13
|
+
- **MSW v2 mock server generation** -- Produce request handlers, mock data factories, and server/browser setup files
|
|
14
|
+
- **CLI tool + programmatic API** -- Use from the terminal or import `generate()` in your build scripts
|
|
15
|
+
- **Watch mode** -- File-watching with debounced regeneration for development workflows
|
|
16
|
+
- **TypeScript-first** -- Fully typed output with per-operation params, body, and response types
|
|
17
|
+
- **One hook per file** -- Maximum tree-shakeability; bundlers only include what you import
|
|
18
|
+
- **Cache key factories** -- React Query key factories following TanStack v5 best practices
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
Install the package:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install auto-api-hooks
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Generate hooks from your API specification:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx auto-api-hooks generate --spec ./openapi.yaml --fetcher react-query --output ./src/hooks
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use the generated hooks in your React components:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { useGetUsers, useCreateUser, configureClient } from './hooks'
|
|
38
|
+
|
|
39
|
+
// Configure the client once at app startup
|
|
40
|
+
configureClient({
|
|
41
|
+
baseUrl: 'https://api.example.com',
|
|
42
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
function UserList() {
|
|
46
|
+
const { data, error, isLoading } = useGetUsers()
|
|
47
|
+
|
|
48
|
+
if (isLoading) return <p>Loading...</p>
|
|
49
|
+
if (error) return <p>Error: {error.message}</p>
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<ul>
|
|
53
|
+
{data?.map((user) => (
|
|
54
|
+
<li key={user.id}>{user.name}</li>
|
|
55
|
+
))}
|
|
56
|
+
</ul>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## CLI Usage
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
auto-api-hooks generate [options]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Flags
|
|
68
|
+
|
|
69
|
+
| Flag | Required | Default | Description |
|
|
70
|
+
|------|----------|---------|-------------|
|
|
71
|
+
| `--spec <path>` | Yes | -- | Path to the API spec file (OpenAPI YAML/JSON, Swagger JSON, GraphQL SDL, or introspection JSON) |
|
|
72
|
+
| `--fetcher <strategy>` | No | `fetch` | Fetching strategy: `fetch`, `axios`, `react-query`, or `swr` |
|
|
73
|
+
| `--output <dir>` | No | `./src/hooks` | Output directory for generated files |
|
|
74
|
+
| `--base-url <url>` | No | From spec | Override the base URL defined in the specification |
|
|
75
|
+
| `--zod` | No | `false` | Generate Zod validation schemas for response types |
|
|
76
|
+
| `--mock` | No | `false` | Generate MSW v2 mock server handlers and data factories |
|
|
77
|
+
| `--watch` | No | `false` | Watch the spec file and regenerate on change |
|
|
78
|
+
| `--no-infinite` | No | -- | Disable automatic infinite query generation for paginated endpoints |
|
|
79
|
+
| `--tag <tags...>` | No | All tags | Filter operations by tag (can specify multiple) |
|
|
80
|
+
| `--verbose` | No | `false` | Enable verbose logging output |
|
|
81
|
+
|
|
82
|
+
### Example Commands
|
|
83
|
+
|
|
84
|
+
**Plain fetch hooks:**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx auto-api-hooks generate \
|
|
88
|
+
--spec ./openapi.yaml \
|
|
89
|
+
--fetcher fetch \
|
|
90
|
+
--output ./src/api
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Axios hooks:**
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx auto-api-hooks generate \
|
|
97
|
+
--spec ./swagger.json \
|
|
98
|
+
--fetcher axios \
|
|
99
|
+
--output ./src/hooks
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**React Query hooks with Zod validation:**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npx auto-api-hooks generate \
|
|
106
|
+
--spec ./openapi.yaml \
|
|
107
|
+
--fetcher react-query \
|
|
108
|
+
--zod \
|
|
109
|
+
--output ./src/hooks
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**SWR hooks with mock server:**
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npx auto-api-hooks generate \
|
|
116
|
+
--spec ./openapi.yaml \
|
|
117
|
+
--fetcher swr \
|
|
118
|
+
--mock \
|
|
119
|
+
--output ./src/hooks
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**GraphQL hooks filtered by tag:**
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx auto-api-hooks generate \
|
|
126
|
+
--spec ./schema.graphql \
|
|
127
|
+
--fetcher react-query \
|
|
128
|
+
--tag queries mutations \
|
|
129
|
+
--output ./src/hooks
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Watch mode for development:**
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx auto-api-hooks generate \
|
|
136
|
+
--spec ./openapi.yaml \
|
|
137
|
+
--fetcher react-query \
|
|
138
|
+
--zod \
|
|
139
|
+
--mock \
|
|
140
|
+
--watch \
|
|
141
|
+
--output ./src/hooks
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Programmatic API
|
|
145
|
+
|
|
146
|
+
Import `generate()` directly for use in build scripts, custom tooling, or CI pipelines:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { generate } from 'auto-api-hooks'
|
|
150
|
+
|
|
151
|
+
const files = await generate({
|
|
152
|
+
spec: './openapi.yaml', // Path to spec file, or a parsed object
|
|
153
|
+
fetcher: 'react-query', // 'fetch' | 'axios' | 'react-query' | 'swr'
|
|
154
|
+
outputDir: './src/hooks', // Write files to disk when provided
|
|
155
|
+
baseUrl: 'https://api.example.com',
|
|
156
|
+
zod: true, // Generate Zod schemas
|
|
157
|
+
mock: true, // Generate MSW mock handlers
|
|
158
|
+
infiniteQueries: true, // Generate infinite query hooks (default: true)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// `files` is an array of { path: string, content: string }
|
|
162
|
+
console.log(`Generated ${files.length} files`)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### GenerateOptions
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
interface GenerateOptions {
|
|
169
|
+
/** Path to the API spec file, or a parsed object. */
|
|
170
|
+
spec: string | object
|
|
171
|
+
/** Fetching strategy. */
|
|
172
|
+
fetcher: 'fetch' | 'axios' | 'react-query' | 'swr'
|
|
173
|
+
/** Output directory. If provided, files are written to disk. */
|
|
174
|
+
outputDir?: string
|
|
175
|
+
/** Override base URL from the spec. */
|
|
176
|
+
baseUrl?: string
|
|
177
|
+
/** Generate Zod validation schemas. */
|
|
178
|
+
zod?: boolean
|
|
179
|
+
/** Generate MSW mock server handlers. */
|
|
180
|
+
mock?: boolean
|
|
181
|
+
/** Generate infinite query hooks for paginated endpoints. Default: true. */
|
|
182
|
+
infiniteQueries?: boolean
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
When `outputDir` is omitted, `generate()` returns the generated files in memory without writing to disk. This is useful for testing or piping output to other tools.
|
|
187
|
+
|
|
188
|
+
### Additional Exports
|
|
189
|
+
|
|
190
|
+
The package also exports lower-level building blocks:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import {
|
|
194
|
+
// Parsing
|
|
195
|
+
parseSpec,
|
|
196
|
+
|
|
197
|
+
// Generation
|
|
198
|
+
generateHooks,
|
|
199
|
+
createGenerator,
|
|
200
|
+
|
|
201
|
+
// Mock generation
|
|
202
|
+
generateMockFiles,
|
|
203
|
+
|
|
204
|
+
// Type emission
|
|
205
|
+
emitTypeScriptTypes,
|
|
206
|
+
emitTypeString,
|
|
207
|
+
emitZodSchemas,
|
|
208
|
+
emitZodType,
|
|
209
|
+
} from 'auto-api-hooks'
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Generated Output Structure
|
|
213
|
+
|
|
214
|
+
A typical generation with `--fetcher react-query --zod --mock` produces the following directory tree:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
src/hooks/
|
|
218
|
+
index.ts # Barrel file re-exporting everything
|
|
219
|
+
client.ts # API client configuration (configureClient, getClientConfig)
|
|
220
|
+
types.ts # TypeScript interfaces for all params, bodies, and responses
|
|
221
|
+
schemas.ts # Zod validation schemas (when --zod is enabled)
|
|
222
|
+
query-keys.ts # Cache key factories (react-query only)
|
|
223
|
+
users/
|
|
224
|
+
index.ts # Barrel for the "users" tag group
|
|
225
|
+
get-users.ts # useGetUsers (useQuery)
|
|
226
|
+
get-users-infinite.ts # useGetUsersInfinite (useInfiniteQuery, when paginated)
|
|
227
|
+
get-user.ts # useGetUser (useQuery)
|
|
228
|
+
create-user.ts # useCreateUser (useMutation)
|
|
229
|
+
update-user.ts # useUpdateUser (useMutation)
|
|
230
|
+
delete-user.ts # useDeleteUser (useMutation)
|
|
231
|
+
posts/
|
|
232
|
+
index.ts
|
|
233
|
+
get-posts.ts
|
|
234
|
+
get-post.ts
|
|
235
|
+
create-post.ts
|
|
236
|
+
mocks/
|
|
237
|
+
index.ts # Mock barrel file
|
|
238
|
+
data.ts # Mock data factory functions
|
|
239
|
+
handlers.ts # MSW v2 request handlers
|
|
240
|
+
server.ts # setupServer() for Node.js (tests, SSR)
|
|
241
|
+
browser.ts # setupWorker() for browser (development)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Operations are grouped by their first tag from the API specification. Each hook lives in its own file for maximum tree-shakeability.
|
|
245
|
+
|
|
246
|
+
## Fetcher Strategies
|
|
247
|
+
|
|
248
|
+
### fetch (Plain)
|
|
249
|
+
|
|
250
|
+
Uses `useState`, `useEffect`, and the native Fetch API. Zero external dependencies beyond React.
|
|
251
|
+
|
|
252
|
+
**Read operations** return `{ data, error, isLoading, refetch }` and include:
|
|
253
|
+
- Built-in `AbortController` support for request cancellation on unmount
|
|
254
|
+
- Automatic refetch when parameters change
|
|
255
|
+
- Optional `enabled` flag to defer fetching
|
|
256
|
+
|
|
257
|
+
**Write operations** return `{ data, error, isLoading, mutate, reset }` with an imperative `mutate()` function.
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import { useGetUsers, useCreateUser } from './hooks'
|
|
261
|
+
|
|
262
|
+
function Example() {
|
|
263
|
+
// Read hook: fetches automatically
|
|
264
|
+
const { data, error, isLoading, refetch } = useGetUsers(
|
|
265
|
+
{ page: 1, limit: 20 },
|
|
266
|
+
{ enabled: true }
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
// Write hook: call mutate() to execute
|
|
270
|
+
const { mutate, isLoading: isCreating } = useCreateUser()
|
|
271
|
+
|
|
272
|
+
const handleCreate = async () => {
|
|
273
|
+
const newUser = await mutate({ name: 'Alice', email: 'alice@example.com' })
|
|
274
|
+
refetch()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ...
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Peer dependencies:** none (only React)
|
|
282
|
+
|
|
283
|
+
### axios
|
|
284
|
+
|
|
285
|
+
Uses an Axios instance with shared configuration. Hooks follow the same `useState`/`useEffect` pattern as the plain fetch strategy but use `apiClient.get()`, `apiClient.post()`, etc.
|
|
286
|
+
|
|
287
|
+
The generated `client.ts` exports a pre-configured Axios instance:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { apiClient, configureClient } from './hooks'
|
|
291
|
+
|
|
292
|
+
// Configure at startup
|
|
293
|
+
configureClient({
|
|
294
|
+
baseUrl: 'https://api.example.com',
|
|
295
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
// Add interceptors directly
|
|
299
|
+
apiClient.interceptors.request.use((config) => {
|
|
300
|
+
// Custom request logic
|
|
301
|
+
return config
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
apiClient.interceptors.response.use(
|
|
305
|
+
(response) => response,
|
|
306
|
+
(error) => {
|
|
307
|
+
if (error.response?.status === 401) {
|
|
308
|
+
// Handle auth errors
|
|
309
|
+
}
|
|
310
|
+
return Promise.reject(error)
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Peer dependencies:** `axios`
|
|
316
|
+
|
|
317
|
+
### react-query (TanStack React Query v5)
|
|
318
|
+
|
|
319
|
+
Full integration with TanStack React Query v5 including:
|
|
320
|
+
|
|
321
|
+
- **`useQuery`** for GET operations with typed query keys
|
|
322
|
+
- **`useMutation`** for POST/PUT/PATCH/DELETE operations
|
|
323
|
+
- **`useInfiniteQuery`** for paginated endpoints (auto-detected)
|
|
324
|
+
- **Cache key factories** per resource following v5 best practices
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
import {
|
|
328
|
+
useGetUsers,
|
|
329
|
+
useGetUsersInfinite,
|
|
330
|
+
useCreateUser,
|
|
331
|
+
userKeys,
|
|
332
|
+
} from './hooks'
|
|
333
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
334
|
+
|
|
335
|
+
function UserList() {
|
|
336
|
+
const queryClient = useQueryClient()
|
|
337
|
+
|
|
338
|
+
// Standard query
|
|
339
|
+
const { data, isLoading } = useGetUsers(
|
|
340
|
+
{ limit: 20 },
|
|
341
|
+
{ staleTime: 5 * 60 * 1000 }
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
// Infinite query for pagination
|
|
345
|
+
const {
|
|
346
|
+
data: infiniteData,
|
|
347
|
+
fetchNextPage,
|
|
348
|
+
hasNextPage,
|
|
349
|
+
} = useGetUsersInfinite({ limit: 20 })
|
|
350
|
+
|
|
351
|
+
// Mutation with cache invalidation
|
|
352
|
+
const createUser = useCreateUser({
|
|
353
|
+
onSuccess: () => {
|
|
354
|
+
queryClient.invalidateQueries({ queryKey: userKeys.lists() })
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
// ...
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Generated cache key factories** (`query-keys.ts`):
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
export const userKeys = {
|
|
368
|
+
all: ['users'] as const,
|
|
369
|
+
lists: () => [...userKeys.all, 'list'] as const,
|
|
370
|
+
list: (params?: Record<string, unknown>) => [...userKeys.lists(), params] as const,
|
|
371
|
+
details: () => [...userKeys.all, 'detail'] as const,
|
|
372
|
+
detail: (id: string | number) => [...userKeys.details(), id] as const,
|
|
373
|
+
} as const
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Peer dependencies:** `@tanstack/react-query` (v5)
|
|
377
|
+
|
|
378
|
+
### swr
|
|
379
|
+
|
|
380
|
+
Integration with Vercel's SWR library:
|
|
381
|
+
|
|
382
|
+
- **`useSWR`** for GET operations with automatic revalidation
|
|
383
|
+
- **`useSWRMutation`** for write operations
|
|
384
|
+
- **`useSWRInfinite`** for paginated endpoints (auto-detected)
|
|
385
|
+
|
|
386
|
+
```tsx
|
|
387
|
+
import { useGetUsers, useGetUsersInfinite, useCreateUser } from './hooks'
|
|
388
|
+
|
|
389
|
+
function UserList() {
|
|
390
|
+
// SWR hook with conditional fetching
|
|
391
|
+
const { data, error, isLoading } = useGetUsers(
|
|
392
|
+
{ limit: 20 },
|
|
393
|
+
{ enabled: true }
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
// Infinite loading
|
|
397
|
+
const { data: pages, size, setSize } = useGetUsersInfinite({ limit: 20 })
|
|
398
|
+
|
|
399
|
+
// Mutation
|
|
400
|
+
const { trigger, isMutating } = useCreateUser()
|
|
401
|
+
const handleCreate = () => {
|
|
402
|
+
trigger({ body: { name: 'Alice', email: 'alice@example.com' } })
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
// ...
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Peer dependencies:** `swr`
|
|
412
|
+
|
|
413
|
+
## Zod Validation
|
|
414
|
+
|
|
415
|
+
When the `--zod` flag is provided, `auto-api-hooks` generates a `schemas.ts` file containing Zod schemas for every named type and every operation response in the specification.
|
|
416
|
+
|
|
417
|
+
### What Gets Generated
|
|
418
|
+
|
|
419
|
+
- **Named type schemas** -- One schema per `components.schemas` entry (OpenAPI) or per named type (GraphQL)
|
|
420
|
+
- **Response schemas** -- One schema per operation response, named `<operationId>ResponseSchema`
|
|
421
|
+
- **Format-aware refinements** -- String formats like `date-time`, `email`, `uuid`, and `uri` are mapped to their corresponding Zod validators
|
|
422
|
+
|
|
423
|
+
Example generated schema:
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
import { z } from 'zod'
|
|
427
|
+
|
|
428
|
+
export const userSchema = z.object({
|
|
429
|
+
id: z.string().uuid(),
|
|
430
|
+
name: z.string(),
|
|
431
|
+
email: z.string().email(),
|
|
432
|
+
createdAt: z.string().datetime(),
|
|
433
|
+
role: z.enum(['admin', 'user', 'guest']),
|
|
434
|
+
avatar: z.string().url().optional(),
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
export const getUsersResponseSchema = z.array(userSchema)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### How It Integrates
|
|
441
|
+
|
|
442
|
+
When Zod is enabled, each generated hook automatically imports its response schema and validates the API response at runtime:
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
// Inside a generated hook (react-query example)
|
|
446
|
+
queryFn: async () => {
|
|
447
|
+
const config = getClientConfig()
|
|
448
|
+
const res = await fetch(url.toString(), { /* ... */ })
|
|
449
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
|
450
|
+
const json = await res.json()
|
|
451
|
+
return getUsersResponseSchema.parse(json) as GetUsersResponse
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
This catches schema mismatches at runtime -- helpful for detecting backend contract drift during development.
|
|
456
|
+
|
|
457
|
+
**Peer dependency when using `--zod`:** `zod`
|
|
458
|
+
|
|
459
|
+
## Mock Server (MSW)
|
|
460
|
+
|
|
461
|
+
The `--mock` flag generates a complete MSW v2 mock server setup, ready for use in tests and browser development.
|
|
462
|
+
|
|
463
|
+
### Generated Files
|
|
464
|
+
|
|
465
|
+
| File | Purpose |
|
|
466
|
+
|------|---------|
|
|
467
|
+
| `mocks/data.ts` | Factory functions that return realistic mock data for each operation |
|
|
468
|
+
| `mocks/handlers.ts` | MSW `http.*` request handlers wired to the data factories |
|
|
469
|
+
| `mocks/server.ts` | `setupServer()` for Node.js environments (tests, SSR) |
|
|
470
|
+
| `mocks/browser.ts` | `setupWorker()` for browser environments (development) |
|
|
471
|
+
| `mocks/index.ts` | Barrel file re-exporting handlers and data factories |
|
|
472
|
+
|
|
473
|
+
### Using in Tests
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
import { server } from './hooks/mocks/server'
|
|
477
|
+
import { beforeAll, afterEach, afterAll } from 'vitest'
|
|
478
|
+
|
|
479
|
+
beforeAll(() => server.listen())
|
|
480
|
+
afterEach(() => server.resetHandlers())
|
|
481
|
+
afterAll(() => server.close())
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Using in the Browser (Development)
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
// src/main.tsx
|
|
488
|
+
async function enableMocking() {
|
|
489
|
+
if (process.env.NODE_ENV !== 'development') return
|
|
490
|
+
const { worker } = await import('./hooks/mocks/browser')
|
|
491
|
+
return worker.start()
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
enableMocking().then(() => {
|
|
495
|
+
// Render your app
|
|
496
|
+
})
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Overriding Individual Handlers
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
import { http, HttpResponse } from 'msw'
|
|
503
|
+
import { server } from './hooks/mocks/server'
|
|
504
|
+
|
|
505
|
+
test('handles server error', async () => {
|
|
506
|
+
server.use(
|
|
507
|
+
http.get('https://api.example.com/users', () => {
|
|
508
|
+
return HttpResponse.json({ message: 'Internal error' }, { status: 500 })
|
|
509
|
+
})
|
|
510
|
+
)
|
|
511
|
+
// Test error handling...
|
|
512
|
+
})
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
**Peer dependency when using `--mock`:** `msw` (v2)
|
|
516
|
+
|
|
517
|
+
## Pagination Detection
|
|
518
|
+
|
|
519
|
+
`auto-api-hooks` automatically analyzes GET and QUERY operations to detect pagination patterns. When pagination is detected, an additional infinite query hook is generated alongside the standard hook (for `react-query` and `swr` strategies).
|
|
520
|
+
|
|
521
|
+
### Detection Heuristics
|
|
522
|
+
|
|
523
|
+
The detection engine inspects both query parameters and response body shape:
|
|
524
|
+
|
|
525
|
+
**Cursor-based pagination:**
|
|
526
|
+
- Query params: `cursor`, `after`, `before`, `page_token`, `pageToken`, `next_token`, `nextToken`, `starting_after`, `startingAfter`, `ending_before`, `endingBefore`
|
|
527
|
+
- Response fields: `nextCursor`, `next_cursor`, `cursor`, `nextPageToken`, `next_page_token`, `nextToken`, `next_token`, `endCursor`, `end_cursor`, `hasMore`, `has_more`
|
|
528
|
+
|
|
529
|
+
**Offset-limit pagination:**
|
|
530
|
+
- Query params: `offset` or `skip` combined with `limit`, `count`, `size`, `per_page`, `perPage`, `page_size`, or `pageSize`
|
|
531
|
+
|
|
532
|
+
**Page-number pagination:**
|
|
533
|
+
- Query params: `page`, `page_number`, `pageNumber`, or `p`
|
|
534
|
+
- Response fields: `totalPages`, `total_pages`, `totalCount`, `total_count`, `total`, `pageCount`, `page_count`, `lastPage`, `last_page`
|
|
535
|
+
|
|
536
|
+
**Response items detection:**
|
|
537
|
+
- Array fields named: `items`, `data`, `results`, `records`, `edges`, `nodes`, `entries`, `list`, `rows`, `content`, `hits`
|
|
538
|
+
- Nested pagination metadata in `pagination`, `meta`, `page_info`, or `pageInfo` objects
|
|
539
|
+
|
|
540
|
+
**GraphQL Relay connections:**
|
|
541
|
+
- Detected via `edges`/`nodes` array fields in the response type
|
|
542
|
+
|
|
543
|
+
### Disabling Pagination Detection
|
|
544
|
+
|
|
545
|
+
To disable infinite query generation entirely:
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
npx auto-api-hooks generate --spec ./openapi.yaml --fetcher react-query --no-infinite
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Or via the programmatic API:
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
await generate({
|
|
555
|
+
spec: './openapi.yaml',
|
|
556
|
+
fetcher: 'react-query',
|
|
557
|
+
infiniteQueries: false,
|
|
558
|
+
})
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
## Supported Spec Formats
|
|
562
|
+
|
|
563
|
+
### OpenAPI 3.x
|
|
564
|
+
|
|
565
|
+
Full support for OpenAPI 3.0 and 3.1 specifications, including:
|
|
566
|
+
|
|
567
|
+
- `$ref` resolution across the document
|
|
568
|
+
- `components.schemas` mapped to TypeScript types and optional Zod schemas
|
|
569
|
+
- `servers[0].url` used as the default base URL
|
|
570
|
+
- All HTTP methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
|
|
571
|
+
- Path parameters, query parameters, and header parameters
|
|
572
|
+
- Request bodies (`application/json`)
|
|
573
|
+
- Response schemas with status codes
|
|
574
|
+
- `deprecated` flag on operations
|
|
575
|
+
- Tag-based grouping of generated hooks
|
|
576
|
+
- `x-pagination` vendor extension for explicit pagination hints
|
|
577
|
+
|
|
578
|
+
**Supported file formats:** `.yaml`, `.yml`, `.json`
|
|
579
|
+
|
|
580
|
+
### Swagger 2.0
|
|
581
|
+
|
|
582
|
+
Full support for Swagger 2.0 specifications, including:
|
|
583
|
+
|
|
584
|
+
- `definitions` mapped to TypeScript types
|
|
585
|
+
- `host` + `basePath` combined into the base URL
|
|
586
|
+
- All standard Swagger features (parameters, responses, tags)
|
|
587
|
+
|
|
588
|
+
**Supported file formats:** `.yaml`, `.yml`, `.json`
|
|
589
|
+
|
|
590
|
+
### GraphQL
|
|
591
|
+
|
|
592
|
+
Support for GraphQL schemas via two input methods:
|
|
593
|
+
|
|
594
|
+
- **SDL schema files** (`.graphql`, `.gql`) -- Parsed with `graphql-js` `buildSchema()`
|
|
595
|
+
- **Introspection JSON** -- Supports both `{ __schema: ... }` and `{ data: { __schema: ... } }` formats
|
|
596
|
+
|
|
597
|
+
Mapping rules:
|
|
598
|
+
|
|
599
|
+
| GraphQL Concept | Generated Hook Type |
|
|
600
|
+
|----------------|---------------------|
|
|
601
|
+
| Query fields | `useQuery` / `useSWR` (GET-equivalent) |
|
|
602
|
+
| Mutation fields | `useMutation` / `useSWRMutation` (POST-equivalent) |
|
|
603
|
+
| Subscription fields | Included in operations (tagged as `subscriptions`) |
|
|
604
|
+
| Object types | TypeScript interfaces |
|
|
605
|
+
| Input types | TypeScript interfaces (used for arguments) |
|
|
606
|
+
| Enum types | TypeScript string unions + Zod enums |
|
|
607
|
+
| Union types | TypeScript union types |
|
|
608
|
+
| Scalar types | Mapped to primitives (`String` -> `string`, `Int` -> `number`, `ID` -> `string`, `DateTime` -> `string` with `date-time` format) |
|
|
609
|
+
|
|
610
|
+
Relay-style connection patterns (`edges`/`nodes`) are detected for automatic infinite query generation.
|
|
611
|
+
|
|
612
|
+
## API Reference
|
|
613
|
+
|
|
614
|
+
### Core Types
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
/** Fetcher strategy identifier. */
|
|
618
|
+
type FetcherStrategy = 'fetch' | 'axios' | 'react-query' | 'swr'
|
|
619
|
+
|
|
620
|
+
/** Options for the generate() function. */
|
|
621
|
+
interface GenerateOptions {
|
|
622
|
+
spec: string | object
|
|
623
|
+
fetcher: FetcherStrategy
|
|
624
|
+
outputDir?: string
|
|
625
|
+
baseUrl?: string
|
|
626
|
+
zod?: boolean
|
|
627
|
+
mock?: boolean
|
|
628
|
+
infiniteQueries?: boolean
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** A generated file ready to be written to disk. */
|
|
632
|
+
interface GeneratedFile {
|
|
633
|
+
/** Relative path from the output directory. */
|
|
634
|
+
path: string
|
|
635
|
+
/** Generated source code content. */
|
|
636
|
+
content: string
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Options for the parseSpec() function. */
|
|
640
|
+
interface ParseOptions {
|
|
641
|
+
baseUrl?: string
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Generator options passed to hook generators. */
|
|
645
|
+
interface GeneratorOptions {
|
|
646
|
+
fetcher: FetcherStrategy
|
|
647
|
+
zod: boolean
|
|
648
|
+
mock: boolean
|
|
649
|
+
outputDir: string
|
|
650
|
+
baseUrl?: string
|
|
651
|
+
infiniteQueries: boolean
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** Interface implemented by all hook generators. */
|
|
655
|
+
interface HookGenerator {
|
|
656
|
+
generate(spec: ApiSpec, options: GeneratorOptions): GeneratedFile[]
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### Intermediate Representation (IR) Types
|
|
661
|
+
|
|
662
|
+
All parsers produce and all generators consume these types:
|
|
663
|
+
|
|
664
|
+
```ts
|
|
665
|
+
/** The complete normalized API specification. */
|
|
666
|
+
interface ApiSpec {
|
|
667
|
+
title: string
|
|
668
|
+
baseUrl: string
|
|
669
|
+
version: string
|
|
670
|
+
operations: ApiOperation[]
|
|
671
|
+
types: Map<string, ApiType>
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** A single API operation. */
|
|
675
|
+
interface ApiOperation {
|
|
676
|
+
operationId: string
|
|
677
|
+
summary?: string
|
|
678
|
+
method: OperationMethod
|
|
679
|
+
path: string
|
|
680
|
+
tags: string[]
|
|
681
|
+
pathParams: ApiParam[]
|
|
682
|
+
queryParams: ApiParam[]
|
|
683
|
+
headerParams: ApiParam[]
|
|
684
|
+
requestBody?: ApiRequestBody
|
|
685
|
+
response: ApiResponse
|
|
686
|
+
pagination?: PaginationInfo
|
|
687
|
+
deprecated: boolean
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'
|
|
691
|
+
type OperationMethod = HttpMethod | 'QUERY' | 'MUTATION' | 'SUBSCRIPTION'
|
|
692
|
+
|
|
693
|
+
/** Recursive type system covering JSON Schema and GraphQL types. */
|
|
694
|
+
type ApiType =
|
|
695
|
+
| ApiPrimitiveType // { kind: 'primitive', type: 'string' | 'number' | ... }
|
|
696
|
+
| ApiObjectType // { kind: 'object', properties: ApiProperty[] }
|
|
697
|
+
| ApiArrayType // { kind: 'array', items: ApiType }
|
|
698
|
+
| ApiEnumType // { kind: 'enum', values: (string | number)[] }
|
|
699
|
+
| ApiUnionType // { kind: 'union', variants: ApiType[] }
|
|
700
|
+
| ApiRefType // { kind: 'ref', name: string }
|
|
701
|
+
|
|
702
|
+
type PaginationStrategy = 'cursor' | 'offset-limit' | 'page-number'
|
|
703
|
+
|
|
704
|
+
interface PaginationInfo {
|
|
705
|
+
strategy: PaginationStrategy
|
|
706
|
+
pageParam: string
|
|
707
|
+
nextPagePath: string[]
|
|
708
|
+
itemsPath: string[]
|
|
709
|
+
}
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
## Configuration
|
|
713
|
+
|
|
714
|
+
### Client Configuration (fetch, react-query, swr)
|
|
715
|
+
|
|
716
|
+
The generated `client.ts` file exports `configureClient()` for setting the base URL and default headers:
|
|
717
|
+
|
|
718
|
+
```ts
|
|
719
|
+
import { configureClient } from './hooks'
|
|
720
|
+
|
|
721
|
+
// Set at app startup
|
|
722
|
+
configureClient({
|
|
723
|
+
baseUrl: 'https://api.example.com/v1',
|
|
724
|
+
headers: {
|
|
725
|
+
Authorization: `Bearer ${getToken()}`,
|
|
726
|
+
'X-Request-ID': crypto.randomUUID(),
|
|
727
|
+
},
|
|
728
|
+
})
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
To update headers dynamically (e.g., after login):
|
|
732
|
+
|
|
733
|
+
```ts
|
|
734
|
+
function onLogin(token: string) {
|
|
735
|
+
configureClient({
|
|
736
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
737
|
+
})
|
|
738
|
+
}
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### Client Configuration (axios)
|
|
742
|
+
|
|
743
|
+
The Axios strategy generates a shared `apiClient` Axios instance. Use it for advanced configuration:
|
|
744
|
+
|
|
745
|
+
```ts
|
|
746
|
+
import { apiClient, configureClient } from './hooks'
|
|
747
|
+
|
|
748
|
+
// Simple configuration
|
|
749
|
+
configureClient({
|
|
750
|
+
baseUrl: 'https://api.example.com/v1',
|
|
751
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
// Advanced: use Axios interceptors
|
|
755
|
+
apiClient.interceptors.request.use((config) => {
|
|
756
|
+
config.headers.Authorization = `Bearer ${getLatestToken()}`
|
|
757
|
+
return config
|
|
758
|
+
})
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Base URL Override
|
|
762
|
+
|
|
763
|
+
You can override the base URL from the spec at generation time:
|
|
764
|
+
|
|
765
|
+
```bash
|
|
766
|
+
npx auto-api-hooks generate \
|
|
767
|
+
--spec ./openapi.yaml \
|
|
768
|
+
--fetcher react-query \
|
|
769
|
+
--base-url https://staging-api.example.com
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
Or at runtime using `configureClient()` as shown above.
|
|
773
|
+
|
|
774
|
+
## Watch Mode
|
|
775
|
+
|
|
776
|
+
Watch mode monitors your spec file for changes and automatically regenerates hooks. It uses `chokidar` with write-finish stabilization (300ms threshold) to avoid regenerating during partial writes.
|
|
777
|
+
|
|
778
|
+
```bash
|
|
779
|
+
npx auto-api-hooks generate \
|
|
780
|
+
--spec ./openapi.yaml \
|
|
781
|
+
--fetcher react-query \
|
|
782
|
+
--zod \
|
|
783
|
+
--watch
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
Watch mode:
|
|
787
|
+
1. Performs an initial generation
|
|
788
|
+
2. Watches the spec file for changes
|
|
789
|
+
3. Debounces rapid file system events (stabilization threshold: 300ms, poll interval: 100ms)
|
|
790
|
+
4. Regenerates all hooks on each detected change
|
|
791
|
+
5. Logs errors without crashing if the spec is temporarily invalid
|
|
792
|
+
6. Stops cleanly on `SIGINT` (Ctrl+C)
|
|
793
|
+
|
|
794
|
+
## Examples
|
|
795
|
+
|
|
796
|
+
### Basic React Query Setup
|
|
797
|
+
|
|
798
|
+
**1. Start with an OpenAPI spec (`openapi.yaml`):**
|
|
799
|
+
|
|
800
|
+
```yaml
|
|
801
|
+
openapi: 3.0.3
|
|
802
|
+
info:
|
|
803
|
+
title: Todo API
|
|
804
|
+
version: 1.0.0
|
|
805
|
+
servers:
|
|
806
|
+
- url: https://api.todo.app
|
|
807
|
+
paths:
|
|
808
|
+
/todos:
|
|
809
|
+
get:
|
|
810
|
+
operationId: getTodos
|
|
811
|
+
tags: [todos]
|
|
812
|
+
parameters:
|
|
813
|
+
- name: status
|
|
814
|
+
in: query
|
|
815
|
+
schema:
|
|
816
|
+
type: string
|
|
817
|
+
enum: [pending, completed]
|
|
818
|
+
responses:
|
|
819
|
+
'200':
|
|
820
|
+
content:
|
|
821
|
+
application/json:
|
|
822
|
+
schema:
|
|
823
|
+
type: array
|
|
824
|
+
items:
|
|
825
|
+
$ref: '#/components/schemas/Todo'
|
|
826
|
+
post:
|
|
827
|
+
operationId: createTodo
|
|
828
|
+
tags: [todos]
|
|
829
|
+
requestBody:
|
|
830
|
+
required: true
|
|
831
|
+
content:
|
|
832
|
+
application/json:
|
|
833
|
+
schema:
|
|
834
|
+
$ref: '#/components/schemas/CreateTodoInput'
|
|
835
|
+
responses:
|
|
836
|
+
'201':
|
|
837
|
+
content:
|
|
838
|
+
application/json:
|
|
839
|
+
schema:
|
|
840
|
+
$ref: '#/components/schemas/Todo'
|
|
841
|
+
/todos/{id}:
|
|
842
|
+
get:
|
|
843
|
+
operationId: getTodo
|
|
844
|
+
tags: [todos]
|
|
845
|
+
parameters:
|
|
846
|
+
- name: id
|
|
847
|
+
in: path
|
|
848
|
+
required: true
|
|
849
|
+
schema:
|
|
850
|
+
type: string
|
|
851
|
+
responses:
|
|
852
|
+
'200':
|
|
853
|
+
content:
|
|
854
|
+
application/json:
|
|
855
|
+
schema:
|
|
856
|
+
$ref: '#/components/schemas/Todo'
|
|
857
|
+
components:
|
|
858
|
+
schemas:
|
|
859
|
+
Todo:
|
|
860
|
+
type: object
|
|
861
|
+
required: [id, title, status]
|
|
862
|
+
properties:
|
|
863
|
+
id:
|
|
864
|
+
type: string
|
|
865
|
+
format: uuid
|
|
866
|
+
title:
|
|
867
|
+
type: string
|
|
868
|
+
status:
|
|
869
|
+
type: string
|
|
870
|
+
enum: [pending, completed]
|
|
871
|
+
createdAt:
|
|
872
|
+
type: string
|
|
873
|
+
format: date-time
|
|
874
|
+
CreateTodoInput:
|
|
875
|
+
type: object
|
|
876
|
+
required: [title]
|
|
877
|
+
properties:
|
|
878
|
+
title:
|
|
879
|
+
type: string
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
**2. Generate hooks:**
|
|
883
|
+
|
|
884
|
+
```bash
|
|
885
|
+
npx auto-api-hooks generate \
|
|
886
|
+
--spec ./openapi.yaml \
|
|
887
|
+
--fetcher react-query \
|
|
888
|
+
--output ./src/hooks
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
**3. Use in your application:**
|
|
892
|
+
|
|
893
|
+
```tsx
|
|
894
|
+
// src/App.tsx
|
|
895
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
896
|
+
import { configureClient } from './hooks'
|
|
897
|
+
import { TodoList } from './components/TodoList'
|
|
898
|
+
|
|
899
|
+
const queryClient = new QueryClient()
|
|
900
|
+
|
|
901
|
+
configureClient({ baseUrl: 'https://api.todo.app' })
|
|
902
|
+
|
|
903
|
+
export function App() {
|
|
904
|
+
return (
|
|
905
|
+
<QueryClientProvider client={queryClient}>
|
|
906
|
+
<TodoList />
|
|
907
|
+
</QueryClientProvider>
|
|
908
|
+
)
|
|
909
|
+
}
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
```tsx
|
|
913
|
+
// src/components/TodoList.tsx
|
|
914
|
+
import { useGetTodos, useCreateTodo, todoKeys } from '../hooks'
|
|
915
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
916
|
+
|
|
917
|
+
export function TodoList() {
|
|
918
|
+
const queryClient = useQueryClient()
|
|
919
|
+
const { data: todos, isLoading } = useGetTodos({ status: 'pending' })
|
|
920
|
+
|
|
921
|
+
const createTodo = useCreateTodo({
|
|
922
|
+
onSuccess: () => {
|
|
923
|
+
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })
|
|
924
|
+
},
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
const handleAdd = () => {
|
|
928
|
+
createTodo.mutate({
|
|
929
|
+
body: { title: 'New task' },
|
|
930
|
+
})
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (isLoading) return <p>Loading todos...</p>
|
|
934
|
+
|
|
935
|
+
return (
|
|
936
|
+
<div>
|
|
937
|
+
<button onClick={handleAdd} disabled={createTodo.isPending}>
|
|
938
|
+
Add Todo
|
|
939
|
+
</button>
|
|
940
|
+
<ul>
|
|
941
|
+
{todos?.map((todo) => (
|
|
942
|
+
<li key={todo.id}>{todo.title} ({todo.status})</li>
|
|
943
|
+
))}
|
|
944
|
+
</ul>
|
|
945
|
+
</div>
|
|
946
|
+
)
|
|
947
|
+
}
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
### Using with Zod + Mock Server
|
|
951
|
+
|
|
952
|
+
This example demonstrates the full development workflow with runtime validation and mocked API responses.
|
|
953
|
+
|
|
954
|
+
**1. Generate with all options:**
|
|
955
|
+
|
|
956
|
+
```bash
|
|
957
|
+
npx auto-api-hooks generate \
|
|
958
|
+
--spec ./openapi.yaml \
|
|
959
|
+
--fetcher react-query \
|
|
960
|
+
--zod \
|
|
961
|
+
--mock \
|
|
962
|
+
--output ./src/hooks
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
**2. Enable mocking in development:**
|
|
966
|
+
|
|
967
|
+
```ts
|
|
968
|
+
// src/main.tsx
|
|
969
|
+
import React from 'react'
|
|
970
|
+
import ReactDOM from 'react-dom/client'
|
|
971
|
+
import { App } from './App'
|
|
972
|
+
|
|
973
|
+
async function main() {
|
|
974
|
+
if (process.env.NODE_ENV === 'development') {
|
|
975
|
+
const { worker } = await import('./hooks/mocks/browser')
|
|
976
|
+
await worker.start({ onUnhandledRequest: 'bypass' })
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
980
|
+
<React.StrictMode>
|
|
981
|
+
<App />
|
|
982
|
+
</React.StrictMode>
|
|
983
|
+
)
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
main()
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
**3. Write tests with the mock server:**
|
|
990
|
+
|
|
991
|
+
```ts
|
|
992
|
+
// src/components/__tests__/TodoList.test.tsx
|
|
993
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
994
|
+
import userEvent from '@testing-library/user-event'
|
|
995
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
996
|
+
import { server } from '../../hooks/mocks/server'
|
|
997
|
+
import { TodoList } from '../TodoList'
|
|
998
|
+
import { beforeAll, afterEach, afterAll, test, expect } from 'vitest'
|
|
999
|
+
|
|
1000
|
+
beforeAll(() => server.listen())
|
|
1001
|
+
afterEach(() => server.resetHandlers())
|
|
1002
|
+
afterAll(() => server.close())
|
|
1003
|
+
|
|
1004
|
+
function renderWithClient(ui: React.ReactElement) {
|
|
1005
|
+
const client = new QueryClient({
|
|
1006
|
+
defaultOptions: { queries: { retry: false } },
|
|
1007
|
+
})
|
|
1008
|
+
return render(
|
|
1009
|
+
<QueryClientProvider client={client}>{ui}</QueryClientProvider>
|
|
1010
|
+
)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
test('renders todo list from mock data', async () => {
|
|
1014
|
+
renderWithClient(<TodoList />)
|
|
1015
|
+
|
|
1016
|
+
await waitFor(() => {
|
|
1017
|
+
expect(screen.queryByText('Loading todos...')).not.toBeInTheDocument()
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
// Mock data from generated factories is rendered
|
|
1021
|
+
expect(screen.getAllByRole('listitem').length).toBeGreaterThan(0)
|
|
1022
|
+
})
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
**4. Zod catches contract drift:**
|
|
1026
|
+
|
|
1027
|
+
If the backend returns data that does not match the schema, the Zod `.parse()` call inside the hook throws a `ZodError` with a detailed path to the invalid field. This surfaces API contract violations immediately during development rather than silently producing incorrect UI state.
|
|
1028
|
+
|
|
1029
|
+
## Support
|
|
1030
|
+
|
|
1031
|
+
If you find this package useful, consider buying me a coffee!
|
|
1032
|
+
|
|
1033
|
+
[](https://buymeacoffee.com/aemadeldin)
|
|
1034
|
+
|
|
1035
|
+
---
|
|
1036
|
+
|
|
1037
|
+
## License
|
|
1038
|
+
|
|
1039
|
+
MIT
|