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/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
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow?style=flat&logo=buy-me-a-coffee)](https://buymeacoffee.com/aemadeldin)
1034
+
1035
+ ---
1036
+
1037
+ ## License
1038
+
1039
+ MIT