@aws/nx-plugin-mcp 0.119.0 → 0.121.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.
Files changed (89) hide show
  1. package/bin/aws-nx-mcp.js +23 -35
  2. package/docs/guides/astro-docs.mdx +142 -0
  3. package/docs/guides/connection/py-agent-a2a.mdx +105 -0
  4. package/docs/guides/connection/py-agent-mcp.mdx +147 -0
  5. package/docs/guides/connection/react-agui.mdx +240 -0
  6. package/docs/guides/connection/react-fastapi.mdx +1476 -0
  7. package/docs/guides/connection/react-py-agent.mdx +198 -0
  8. package/docs/guides/connection/react-smithy.mdx +1355 -0
  9. package/docs/guides/connection/react-trpc.mdx +447 -0
  10. package/docs/guides/connection/react-ts-agent.mdx +198 -0
  11. package/docs/guides/connection/smithy-rdb.mdx +161 -0
  12. package/docs/guides/connection/trpc-rdb.mdx +127 -0
  13. package/docs/guides/connection/ts-agent-a2a.mdx +106 -0
  14. package/docs/guides/connection/ts-agent-mcp.mdx +144 -0
  15. package/docs/guides/connection/ts-agent-rdb.mdx +141 -0
  16. package/docs/guides/connection/ts-mcp-server-rdb.mdx +135 -0
  17. package/docs/guides/connection.mdx +126 -0
  18. package/docs/guides/docker-bundling.mdx +403 -0
  19. package/docs/guides/fastapi.mdx +509 -0
  20. package/docs/guides/license.mdx +293 -0
  21. package/docs/guides/nx-generator.mdx +588 -0
  22. package/docs/guides/py-agent.mdx +483 -0
  23. package/docs/guides/py-mcp-server.mdx +161 -0
  24. package/docs/guides/python-lambda-function.mdx +207 -0
  25. package/docs/guides/python-project.mdx +228 -0
  26. package/docs/guides/react-website-auth.mdx +216 -0
  27. package/docs/guides/react-website.mdx +421 -0
  28. package/docs/guides/runtime-config.mdx +312 -0
  29. package/docs/guides/terraform-project.mdx +317 -0
  30. package/docs/guides/trpc.mdx +816 -0
  31. package/docs/guides/ts-agent.mdx +436 -0
  32. package/docs/guides/ts-lambda-function.mdx +218 -0
  33. package/docs/guides/ts-mcp-server.mdx +169 -0
  34. package/docs/guides/ts-nx-plugin.mdx +159 -0
  35. package/docs/guides/ts-rdb.mdx +759 -0
  36. package/docs/guides/ts-smithy-api.mdx +661 -0
  37. package/docs/guides/typescript-infrastructure.mdx +408 -0
  38. package/docs/guides/typescript-project.mdx +312 -0
  39. package/docs/guides/workspace.mdx +181 -0
  40. package/docs/snippets/agent/architecture.mdx +72 -0
  41. package/docs/snippets/agent/bedrock-deployment.mdx +172 -0
  42. package/docs/snippets/agent/runtime-arn.mdx +64 -0
  43. package/docs/snippets/api/api-architecture.mdx +93 -0
  44. package/docs/snippets/api/api-choice-note.mdx +6 -0
  45. package/docs/snippets/api/cors-configuration-cdk-note.mdx +25 -0
  46. package/docs/snippets/api/cors-configuration-terraform-note.mdx +28 -0
  47. package/docs/snippets/api/shared-constructs.mdx +38 -0
  48. package/docs/snippets/api/type-safe-api-integrations.mdx +643 -0
  49. package/docs/snippets/api/waf-configuration.mdx +37 -0
  50. package/docs/snippets/connection/a2a-infrastructure.mdx +63 -0
  51. package/docs/snippets/connection/lambda-rdb-ssl-requirements.mdx +40 -0
  52. package/docs/snippets/connection/mcp-server-rdb-ssl-requirements.mdx +35 -0
  53. package/docs/snippets/connection/rdb-api-infrastructure.mdx +72 -0
  54. package/docs/snippets/connection/react-agent-infrastructure.mdx +61 -0
  55. package/docs/snippets/connection/strands-agent-rdb-ssl-requirements.mdx +35 -0
  56. package/docs/snippets/lambda-function/architecture.mdx +36 -0
  57. package/docs/snippets/lambda-function/deploying-your-function.mdx +118 -0
  58. package/docs/snippets/mcp/architecture.mdx +58 -0
  59. package/docs/snippets/mcp/assistant-docs.mdx +10 -0
  60. package/docs/snippets/mcp/bedrock-deployment.mdx +167 -0
  61. package/docs/snippets/mcp/config.mdx +13 -0
  62. package/docs/snippets/mcp/configuration-py.mdx +42 -0
  63. package/docs/snippets/mcp/configuration-ts.mdx +53 -0
  64. package/docs/snippets/mcp/observability.mdx +8 -0
  65. package/docs/snippets/mcp/shared-constructs.mdx +32 -0
  66. package/docs/snippets/pdk-migration/example/01-migrate-api.mdx +602 -0
  67. package/docs/snippets/pdk-migration/example/02-migrate-website.mdx +915 -0
  68. package/docs/snippets/pdk-migration/example/03-migrate-infra.mdx +161 -0
  69. package/docs/snippets/pdk-migration/example/04-deploy.mdx +229 -0
  70. package/docs/snippets/pdk-migration/faq/aws-arch.mdx +17 -0
  71. package/docs/snippets/pdk-migration/faq/cdk-graph.mdx +29 -0
  72. package/docs/snippets/pdk-migration/faq/infrastructure-python-java.mdx +19 -0
  73. package/docs/snippets/pdk-migration/faq/pdk-nag.mdx +15 -0
  74. package/docs/snippets/pdk-migration/faq/pipeline.mdx +15 -0
  75. package/docs/snippets/pdk-migration/faq/type-safe-api.mdx +310 -0
  76. package/docs/snippets/pdk-migration/faq/use-of-projen.mdx +15 -0
  77. package/docs/snippets/prerequisites.mdx +20 -0
  78. package/docs/snippets/required-prerequisites.mdx +12 -0
  79. package/docs/snippets/shared-constructs.mdx +40 -0
  80. package/docs/snippets/tools/acurl.mdx +73 -0
  81. package/docs/snippets/ts-bundle.mdx +14 -0
  82. package/package.json +1 -1
  83. package/src/py/agent/schema.json +1 -1
  84. package/src/py/fast-api/schema.json +2 -2
  85. package/src/py/mcp-server/schema.json +1 -1
  86. package/src/smithy/ts/api/schema.json +2 -2
  87. package/src/trpc/backend/schema.json +2 -2
  88. package/src/ts/agent/schema.json +1 -1
  89. package/src/ts/mcp-server/schema.json +1 -1
@@ -0,0 +1,1476 @@
1
+ ---
2
+ title: React to FastAPI
3
+ description: Connect a React website to a Python FastAPI
4
+ when:
5
+ sourceType: react
6
+ targetType: py#fast-api
7
+ ---
8
+
9
+ import { FileTree, Steps } from '@astrojs/starlight/components';
10
+ import Link from '@components/link.astro';
11
+ import RunGenerator from '@components/run-generator.astro';
12
+ import NxCommands from '@components/nx-commands.astro';
13
+ import PackageManagerShortCommand from '@components/package-manager-short-command.astro';
14
+ import Drawer from '@components/drawer.astro';
15
+ import GeneratorParameters from '@components/generator-parameters.astro';
16
+
17
+ The `connection` generator provides a way to quickly integrate your React website with your FastAPI backend. It sets up all necessary configuration for connecting to your FastAPI backends in a type-safe manner, including client and [TanStack Query](https://tanstack.com/query/v5) hooks generation, AWS IAM and Cognito authentication support and proper error handling.
18
+
19
+ ## Prerequisites
20
+
21
+ Before using this generator, ensure your React application has:
22
+
23
+ 1. A `main.tsx` file that renders your application
24
+ 2. A working FastAPI backend (generated using the FastAPI generator)
25
+ 3. Cognito Auth added via the <Link path="/guides/react-website-auth">`ts#react-website-auth` generator</Link> if connecting an API which uses Cognito or IAM auth
26
+
27
+ <details>
28
+ <summary>Example of required `main.tsx` structure</summary>
29
+
30
+ ```tsx
31
+ import { StrictMode } from 'react';
32
+ import * as ReactDOM from 'react-dom/client';
33
+ import App from './app/app';
34
+
35
+ const root = ReactDOM.createRoot(
36
+ document.getElementById('root') as HTMLElement,
37
+ );
38
+ root.render(
39
+ <StrictMode>
40
+ <App />
41
+ </StrictMode>,
42
+ );
43
+ ```
44
+
45
+ </details>
46
+
47
+ ## Usage
48
+
49
+ ### Run the Generator
50
+
51
+ <RunGenerator generator="connection" />
52
+
53
+ ### Options
54
+
55
+ <GeneratorParameters generator="connection" />
56
+
57
+ ## Generator Output
58
+
59
+ The generator will make changes to the following files in your FastAPI project:
60
+
61
+ <FileTree>
62
+
63
+ - scripts
64
+ - generate_open_api.py Add a script which generates an OpenAPI specification for your API
65
+ - project.json A new target is added to the build which invokes the above generate script
66
+
67
+ </FileTree>
68
+
69
+ The generator will make changes to the following files in your React application:
70
+
71
+ <FileTree>
72
+
73
+ - src
74
+ - components
75
+ - \<ApiName>Provider.tsx Provider for your API client
76
+ - QueryClientProvider.tsx TanStack React Query client provider
77
+ - hooks
78
+ - use\<ApiName>.tsx Add a hook for calling your API with state managed by TanStack Query
79
+ - use\<ApiName>Client.tsx Add a hook for instantiating the vanilla API client which can call your API.
80
+ - useSigV4.tsx Add a hook for signing HTTP requests with SigV4 (if you selected IAM authentication)
81
+ - project.json A new target is added to the build which generates a type-safe client
82
+ - .gitignore The generated client files are ignored by default
83
+
84
+ </FileTree>
85
+
86
+ The generator will also add Runtime Config to your website infrastructure if not present already, which ensures that the API URL for your FastAPI is available in the website and automatically configured by the `use<ApiName>.tsx` hook.
87
+
88
+ ### Code Generation
89
+
90
+ At build time, a type-safe client is generated from your FastAPI's OpenAPI specification. This will add three new files to your React application:
91
+
92
+ <FileTree>
93
+
94
+ - src
95
+ - generated
96
+ - \<ApiName>
97
+ - types.gen.ts Generated types from the pydantic models defined in your FastAPI
98
+ - client.gen.ts Type-safe client for calling your API
99
+ - options-proxy.gen.ts Provides methods to create TanStack Query hooks options for interacting with your API using TanStack Query
100
+
101
+ </FileTree>
102
+
103
+ :::tip[Generated Client in Git]
104
+ By default, the generated client is ignored from version control. If you would prefer to check it in, you can remove the entry from your React application's `.gitignore` file, however note that any manual changes to the `.gen.ts` files will be overwritten when your project is built.
105
+ :::
106
+
107
+ ## Using the Generated Code
108
+
109
+ The generated type-safe client can be used to call your FastAPI from your React application. It's recommended to make use of the client via the TanStack Query hooks, but you can use the vanilla client if you prefer.
110
+
111
+ :::note[Regenerating Client Code]
112
+ Whenever you make changes to your FastAPI, you need to rebuild your project in order for those changes to be reflected in the generated client. For example:
113
+
114
+ <PackageManagerShortCommand commands={["build"]} />
115
+ :::
116
+
117
+ :::tip[Auto-Regeneration]
118
+ If you're actively working on both your React application and FastAPI together, use the React application's `serve-local` target which will automatically regenerate the client whenever your API changes, as well as hot-reloading your website and local FastAPI server:
119
+
120
+ <NxCommands commands={['serve-local <WebsiteProject>']} />
121
+
122
+ For more fine-grained control, you can use the `watch-generate:<ApiName>-client` target for your React application to regenerate the client every time you make API changes:
123
+
124
+ <NxCommands commands={['run <WebsiteProject>:"watch-generate:<ApiName>-client"']}
125
+ />
126
+ :::
127
+
128
+ :::warning[File Watcher Dependency]
129
+ `watch-generate:<ApiName>-client` relies on the [`nx watch`](https://nx.dev/docs/guides/tasks--caching/workspace-watching) command, which requires the [Nx Daemon](https://nx.dev/docs/concepts/nx-daemon) to be running. Therefore if you have disabled the daemon, the client won't automatically regenerate when making changes to your FastAPI.
130
+ :::
131
+
132
+ ### Using the API Hook
133
+
134
+ The generator provides a `use<ApiName>` hook which you can use to call your API with TanStack Query.
135
+
136
+ ### Queries
137
+
138
+ You can use the `queryOptions` method to retrieve the options required for calling your API using TanStack Query's `useQuery` hook:
139
+
140
+ ```tsx {7}
141
+ import { useQuery } from '@tanstack/react-query';
142
+ import { useState, useEffect } from 'react';
143
+ import { useMyApi } from './hooks/useMyApi';
144
+
145
+ function MyComponent() {
146
+ const api = useMyApi();
147
+ const item = useQuery(api.getItem.queryOptions({ itemId: 'some-id' }));
148
+
149
+ if (item.isLoading) return <div>Loading...</div>;
150
+ if (item.isError) return <div>Error: {item.error.message}</div>;
151
+
152
+ return <div>Item: {item.data.name}</div>;
153
+ }
154
+ ```
155
+
156
+ <Drawer title="Using the API client directly" trigger="Click here for an example using the vanilla client directly.">
157
+ ```tsx {5,13}
158
+ import { useState, useEffect } from 'react';
159
+ import { useMyApiClient } from './hooks/useMyApiClient';
160
+
161
+ function MyComponent() {
162
+ const api = useMyApiClient();
163
+ const [item, setItem] = useState(null);
164
+ const [loading, setLoading] = useState(true);
165
+ const [error, setError] = useState(null);
166
+
167
+ useEffect(() => {
168
+ const fetchItem = async () => {
169
+ try {
170
+ const data = await api.getItem({ itemId: 'some-id' });
171
+ setItem(data);
172
+ } catch (err) {
173
+ setError(err);
174
+ } finally {
175
+ setLoading(false);
176
+ }
177
+ };
178
+ fetchItem();
179
+ }, [api]);
180
+
181
+ if (loading) return <div>Loading...</div>;
182
+ if (error) return <div>Error: {error.message}</div>;
183
+
184
+ return <div>Item: {item.name}</div>;
185
+ }
186
+ ```
187
+ </Drawer>
188
+
189
+ ### Mutations
190
+
191
+ The generated hooks include support for mutations using TanStack Query's `useMutation` hook. This provides a clean way to handle create, update, and delete operations with loading states, error handling, and optimistic updates.
192
+
193
+ ```tsx {5-7,11}
194
+ import { useMutation } from '@tanstack/react-query';
195
+ import { useMyApi } from './hooks/useMyApi';
196
+
197
+ function CreateItemForm() {
198
+ const api = useMyApi();
199
+ // Create a mutation using the generated mutation options
200
+ const createItem = useMutation(api.createItem.mutationOptions());
201
+
202
+ const handleSubmit = (e) => {
203
+ e.preventDefault();
204
+ createItem.mutate({ name: 'New Item', description: 'A new item' });
205
+ };
206
+
207
+ return (
208
+ <form onSubmit={handleSubmit}>
209
+ {/* Form fields */}
210
+ <button
211
+ type="submit"
212
+ disabled={createItem.isPending}
213
+ >
214
+ {createItem.isPending ? 'Creating...' : 'Create Item'}
215
+ </button>
216
+
217
+ {createItem.isSuccess && (
218
+ <div className="success">
219
+ Item created with ID: {createItem.data.id}
220
+ </div>
221
+ )}
222
+
223
+ {createItem.isError && (
224
+ <div className="error">
225
+ Error: {createItem.error.message}
226
+ </div>
227
+ )}
228
+ </form>
229
+ );
230
+ }
231
+ ```
232
+
233
+ You can also add callbacks for different mutation states:
234
+
235
+ ```tsx
236
+ const createItem = useMutation({
237
+ ...api.createItem.mutationOptions(),
238
+ onSuccess: (data) => {
239
+ // This will run when the mutation succeeds
240
+ console.log('Item created:', data);
241
+ // You can navigate to the new item
242
+ navigate(`/items/${data.id}`);
243
+ },
244
+ onError: (error) => {
245
+ // This will run when the mutation fails
246
+ console.error('Failed to create item:', error);
247
+ },
248
+ onSettled: () => {
249
+ // This will run when the mutation completes (success or error)
250
+ // Good place to invalidate queries that might be affected
251
+ queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
252
+ }
253
+ });
254
+ ```
255
+
256
+ <Drawer title="Mutations using the API client directly" trigger="Click here for an example using the client directly.">
257
+ ```tsx
258
+ import { useState } from 'react';
259
+ import { useMyApiClient } from './hooks/useMyApiClient';
260
+
261
+ function CreateItemForm() {
262
+ const api = useMyApiClient();
263
+ const [isLoading, setIsLoading] = useState(false);
264
+ const [error, setError] = useState(null);
265
+ const [createdItem, setCreatedItem] = useState(null);
266
+
267
+ const handleSubmit = async (e) => {
268
+ e.preventDefault();
269
+ setIsLoading(true);
270
+ setError(null);
271
+
272
+ try {
273
+ const newItem = await api.createItem({
274
+ name: 'New Item',
275
+ description: 'A new item'
276
+ });
277
+ setCreatedItem(newItem);
278
+ // You can navigate to the new item
279
+ // navigate(`/items/${newItem.id}`);
280
+ } catch (err) {
281
+ setError(err);
282
+ console.error('Failed to create item:', err);
283
+ } finally {
284
+ setIsLoading(false);
285
+ }
286
+ };
287
+
288
+ return (
289
+ <form onSubmit={handleSubmit}>
290
+ {/* Form fields */}
291
+ <button
292
+ type="submit"
293
+ disabled={isLoading}
294
+ >
295
+ {isLoading ? 'Creating...' : 'Create Item'}
296
+ </button>
297
+
298
+ {createdItem && (
299
+ <div className="success">
300
+ Item created with ID: {createdItem.id}
301
+ </div>
302
+ )}
303
+
304
+ {error && (
305
+ <div className="error">
306
+ Error: {error.message}
307
+ </div>
308
+ )}
309
+ </form>
310
+ );
311
+ }
312
+ ```
313
+ </Drawer>
314
+
315
+ ### Pagination with Infinite Queries
316
+
317
+ For endpoints that accept a `cursor` parameter as input, the generated hooks provide support for infinite queries using TanStack Query's `useInfiniteQuery` hook. This makes it easy to implement "load more" or infinite scrolling functionality.
318
+
319
+ ```tsx {6-16,30-34}
320
+ import { useInfiniteQuery } from '@tanstack/react-query';
321
+ import { useMyApi } from './hooks/useMyApi';
322
+
323
+ function ItemList() {
324
+ const api = useMyApi();
325
+ const items = useInfiniteQuery({
326
+ ...api.listItems.infiniteQueryOptions({
327
+ limit: 10, // Number of items per page
328
+ }, {
329
+ // Make sure you define a getNextPageParam function to return
330
+ // the parameter that should be passed as the 'cursor' for the
331
+ // next page
332
+ getNextPageParam: (lastPage) =>
333
+ lastPage.nextCursor || undefined
334
+ }),
335
+ });
336
+
337
+ if (items.isLoading) {
338
+ return <LoadingSpinner />;
339
+ }
340
+
341
+ if (items.isError) {
342
+ return <ErrorMessage message={items.error.message} />;
343
+ }
344
+
345
+ return (
346
+ <div>
347
+ {/* Flatten the pages array to render all items */}
348
+ <ul>
349
+ {items.data.pages.flatMap(page =>
350
+ page.items.map(item => (
351
+ <li key={item.id}>{item.name}</li>
352
+ ))
353
+ )}
354
+ </ul>
355
+
356
+ <button
357
+ onClick={() => items.fetchNextPage()}
358
+ disabled={!items.hasNextPage || items.isFetchingNextPage}
359
+ >
360
+ {items.isFetchingNextPage
361
+ ? 'Loading more...'
362
+ : items.hasNextPage
363
+ ? 'Load More'
364
+ : 'No more items'}
365
+ </button>
366
+ </div>
367
+ );
368
+ }
369
+ ```
370
+
371
+ The generated hooks automatically handle cursor-based pagination if your API supports it. The `nextCursor` value is extracted from the response and used to fetch the next page.
372
+
373
+ :::tip[Paginated APIs]
374
+ If you have a paginated API which has its pagination parameter named something other than `cursor`, you can [customise it using the `x-cursor` OpenAPI vendor extension](#custom-pagination-cursor).
375
+ :::
376
+
377
+ <Drawer title="Pagination using the API client directly" trigger="Click here for an example using the client directly.">
378
+ ```tsx
379
+ import { useState, useEffect } from 'react';
380
+ import { useMyApiClient } from './hooks/useMyApiClient';
381
+
382
+ function ItemList() {
383
+ const api = useMyApiClient();
384
+ const [items, setItems] = useState([]);
385
+ const [isLoading, setIsLoading] = useState(true);
386
+ const [error, setError] = useState(null);
387
+ const [nextCursor, setNextCursor] = useState(null);
388
+ const [isFetchingMore, setIsFetchingMore] = useState(false);
389
+
390
+ // Fetch initial data
391
+ useEffect(() => {
392
+ const fetchItems = async () => {
393
+ try {
394
+ setIsLoading(true);
395
+ const response = await api.listItems({ limit: 10 });
396
+ setItems(response.items);
397
+ setNextCursor(response.nextCursor);
398
+ } catch (err) {
399
+ setError(err);
400
+ } finally {
401
+ setIsLoading(false);
402
+ }
403
+ };
404
+
405
+ fetchItems();
406
+ }, [api]);
407
+
408
+ // Function to load more items
409
+ const loadMore = async () => {
410
+ if (!nextCursor) return;
411
+
412
+ try {
413
+ setIsFetchingMore(true);
414
+ const response = await api.listItems({
415
+ limit: 10,
416
+ cursor: nextCursor
417
+ });
418
+
419
+ setItems(prevItems => [...prevItems, ...response.items]);
420
+ setNextCursor(response.nextCursor);
421
+ } catch (err) {
422
+ setError(err);
423
+ } finally {
424
+ setIsFetchingMore(false);
425
+ }
426
+ };
427
+
428
+ if (isLoading) {
429
+ return <LoadingSpinner />;
430
+ }
431
+
432
+ if (error) {
433
+ return <ErrorMessage message={error.message} />;
434
+ }
435
+
436
+ return (
437
+ <div>
438
+ <ul>
439
+ {items.map(item => (
440
+ <li key={item.id}>{item.name}</li>
441
+ ))}
442
+ </ul>
443
+
444
+ <button
445
+ onClick={loadMore}
446
+ disabled={!nextCursor || isFetchingMore}
447
+ >
448
+ {isFetchingMore
449
+ ? 'Loading more...'
450
+ : nextCursor
451
+ ? 'Load More'
452
+ : 'No more items'}
453
+ </button>
454
+ </div>
455
+ );
456
+ }
457
+ ```
458
+ </Drawer>
459
+
460
+ ### Error Handling
461
+
462
+ The integration includes built-in error handling with typed error responses. An `<operation-name>Error` type is generated which encapsulates the possible error responses defined in the OpenAPI specification. Each error has a `status` and `error` property, and by checking the value of `status` you can narrow to a specific type of error.
463
+
464
+ ```tsx {12}
465
+ import { useMutation } from '@tanstack/react-query';
466
+
467
+ function MyComponent() {
468
+ const api = useMyApi();
469
+ const createItem = useMutation(api.createItem.mutationOptions());
470
+
471
+ const handleClick = () => {
472
+ createItem.mutate({ name: 'New Item' });
473
+ };
474
+
475
+ if (createItem.error) {
476
+ switch (createItem.error.status) {
477
+ case 400:
478
+ // error.error is typed as CreateItem400Response
479
+ return (
480
+ <div>
481
+ <h2>Invalid input:</h2>
482
+ <p>{createItem.error.error.message}</p>
483
+ <ul>
484
+ {createItem.error.error.validationErrors.map((err) => (
485
+ <li key={err.field}>{err.message}</li>
486
+ ))}
487
+ </ul>
488
+ </div>
489
+ );
490
+ case 403:
491
+ // error.error is typed as CreateItem403Response
492
+ return (
493
+ <div>
494
+ <h2>Not authorized:</h2>
495
+ <p>{createItem.error.error.reason}</p>
496
+ </div>
497
+ );
498
+ case 500:
499
+ case 502:
500
+ // error.error is typed as CreateItem5XXResponse
501
+ return (
502
+ <div>
503
+ <h2>Server error:</h2>
504
+ <p>{createItem.error.error.message}</p>
505
+ <p>Trace ID: {createItem.error.error.traceId}</p>
506
+ </div>
507
+ );
508
+ }
509
+ }
510
+
511
+ return <button onClick={handleClick}>Create Item</button>;
512
+ }
513
+ ```
514
+
515
+ <Drawer title="Error handling using the API client directly" trigger="Click here for an example using the vanilla client directly.">
516
+ ```tsx {9,15}
517
+ function MyComponent() {
518
+ const api = useMyApiClient();
519
+ const [error, setError] = useState<CreateItemError | null>(null);
520
+
521
+ const handleClick = async () => {
522
+ try {
523
+ await api.createItem({ name: 'New Item' });
524
+ } catch (e) {
525
+ const err = e as CreateItemError;
526
+ setError(err);
527
+ }
528
+ };
529
+
530
+ if (error) {
531
+ switch (error.status) {
532
+ case 400:
533
+ // error.error is typed as CreateItem400Response
534
+ return (
535
+ <div>
536
+ <h2>Invalid input:</h2>
537
+ <p>{error.error.message}</p>
538
+ <ul>
539
+ {error.error.validationErrors.map((err) => (
540
+ <li key={err.field}>{err.message}</li>
541
+ ))}
542
+ </ul>
543
+ </div>
544
+ );
545
+ case 403:
546
+ // error.error is typed as CreateItem403Response
547
+ return (
548
+ <div>
549
+ <h2>Not authorized:</h2>
550
+ <p>{error.error.reason}</p>
551
+ </div>
552
+ );
553
+ case 500:
554
+ case 502:
555
+ // error.error is typed as CreateItem5XXResponse
556
+ return (
557
+ <div>
558
+ <h2>Server error:</h2>
559
+ <p>{error.error.message}</p>
560
+ <p>Trace ID: {error.error.traceId}</p>
561
+ </div>
562
+ );
563
+ }
564
+ }
565
+
566
+ return <button onClick={handleClick}>Create Item</button>;
567
+ }
568
+ ```
569
+ </Drawer>
570
+
571
+ ### Consuming a Stream
572
+
573
+ If you have <Link path="guides/fastapi#streaming">configured your FastAPI to stream responses</Link>, your `useQuery` hook will automatically update its data as new chunks of the stream arrive.
574
+
575
+ For example:
576
+
577
+ ```tsx {3}
578
+ function MyStreamingComponent() {
579
+ const api = useMyApi();
580
+ const stream = useQuery(api.myStream.queryOptions());
581
+
582
+ return (
583
+ <ul>
584
+ {(stream.data ?? []).map((chunk) => (
585
+ <li>
586
+ {chunk.timestamp.toISOString()}: {chunk.message}
587
+ </li>
588
+ ))}
589
+ </ul>
590
+ );
591
+ }
592
+ ```
593
+
594
+ You can use the `isLoading` and `fetchStatus` properties to determine the current state of the stream if necessary. A stream follows this lifecycle:
595
+
596
+ <Steps>
597
+ 1. The HTTP request to start streaming is sent
598
+
599
+ - `isLoading` is `true`
600
+ - `fetchStatus` is `'fetching'`
601
+ - `data` is `undefined`
602
+
603
+ 2. The first chunk of the stream is received
604
+
605
+ - `isLoading` becomes `false`
606
+ - `fetchStatus` remains `'fetching'`
607
+ - `data` becomes an array containing the first chunk
608
+
609
+ 3. Subsequent chunks are received
610
+
611
+ - `isLoading` remains `false`
612
+ - `fetchStatus` remains `'fetching'`
613
+ - `data` is updated with each subsequent chunk as soon as it is received
614
+
615
+ 4. The stream completes
616
+
617
+ - `isLoading` remains `false`
618
+ - `fetchStatus` becomes `'idle'`
619
+ - `data` is an array of all received chunks
620
+ </Steps>
621
+
622
+ <Drawer title="Streaming using the API client directly" trigger="Click here for an example using the vanilla client directly.">
623
+
624
+ If you have <Link path="guides/fastapi#streaming">configured your FastAPI to stream responses</Link>>, the generated client will include type-safe methods for asynchronously iterating over chunks in your stream using `for await` syntax.
625
+
626
+ For example:
627
+
628
+ ```tsx {8}
629
+ function MyStreamingComponent() {
630
+ const api = useMyApiClient();
631
+
632
+ const [chunks, setChunks] = useState<Chunk[]>([]);
633
+
634
+ useEffect(() => {
635
+ const streamChunks = async () => {
636
+ for await (const chunk of api.myStream()) {
637
+ setChunks((prev) => [...prev, chunk]);
638
+ }
639
+ };
640
+ streamChunks();
641
+ }, [api]);
642
+
643
+ return (
644
+ <ul>
645
+ {chunks.map((chunk) => (
646
+ <li>
647
+ {chunk.timestamp.toISOString()}: {chunk.message}
648
+ </li>
649
+ ))}
650
+ </ul>
651
+ );
652
+ }
653
+ ```
654
+ </Drawer>
655
+
656
+ :::note[Streaming APIs]
657
+ If you have a streaming API which accepts a `cursor` parameter, when using the `useInfiniteQuery` hook, each page will wait for the stream to finish before it has been loaded.
658
+ :::
659
+
660
+ ## Customising the Generated Code
661
+
662
+ ### Queries and Mutations
663
+
664
+ By default, operations in your FastAPI which use the HTTP methods `PUT`, `POST`, `PATCH` and `DELETE` are considered mutations, and all others are considered queries.
665
+
666
+ You can change this behaviour using `x-query` and `x-mutation`.
667
+
668
+ #### x-query
669
+
670
+ ```python
671
+ @app.post(
672
+ "/items",
673
+ openapi_extra={
674
+ "x-query": True
675
+ }
676
+ )
677
+ def list_items():
678
+ # ...
679
+ ```
680
+
681
+ The generated hook will provide `queryOptions` even though it uses the `POST` HTTP method:
682
+
683
+ ```tsx
684
+ const items = useQuery(api.listItems.queryOptions());
685
+ ```
686
+
687
+ #### x-mutation
688
+
689
+ ```python
690
+ @app.get(
691
+ "/start-processing",
692
+ openapi_extra={
693
+ "x-mutation": True
694
+ }
695
+ )
696
+ def start_processing():
697
+ # ...
698
+ ```
699
+
700
+ The generated hook will provide `mutationOptions` even though it uses the `GET` HTTP method:
701
+
702
+ ```tsx
703
+ // Generated hook will include the custom options
704
+ const startProcessing = useMutation(api.startProcessing.mutationOptions());
705
+ ```
706
+
707
+ ### Custom Pagination Cursor
708
+
709
+ By default, the generated hooks assume cursor-based pagination with a parameter named `cursor`. You can customize this behavior using the `x-cursor` extension:
710
+
711
+ ```python
712
+ @app.get(
713
+ "/items",
714
+ openapi_extra={
715
+ # Specify a different parameter name for the cursor
716
+ "x-cursor": "page_token"
717
+ }
718
+ )
719
+ def list_items(page_token: str = None, limit: int = 10):
720
+ # ...
721
+ return {
722
+ "items": items,
723
+ "page_token": next_page_token
724
+ }
725
+ ```
726
+
727
+ If you would not like to generate `infiniteQueryOptions` for an operation, you can set `x-cursor` to `False`:
728
+
729
+ ```python
730
+ @app.get(
731
+ "/items",
732
+ openapi_extra={
733
+ # Disable cursor-based pagination for this endpoint
734
+ "x-cursor": False
735
+ }
736
+ )
737
+ def list_items(page: int = 1, limit: int = 10):
738
+ # ...
739
+ return {
740
+ "items": items,
741
+ "total": total_count,
742
+ "page": page,
743
+ "pages": total_pages
744
+ }
745
+ ```
746
+
747
+ ### Grouping Operations
748
+
749
+ The generated hooks and client methods are automatically organized based on the OpenAPI tags in your FastAPI endpoints. This helps keep your API calls organized and makes it easier to find related operations.
750
+
751
+ For example:
752
+
753
+ ```python title="items.py"
754
+ @app.get(
755
+ "/items",
756
+ tags=["items"],
757
+ )
758
+ def list():
759
+ # ...
760
+
761
+ @app.post(
762
+ "/items",
763
+ tags=["items"],
764
+ )
765
+ def create(item: Item):
766
+ # ...
767
+ ```
768
+
769
+ ```python title="users.py"
770
+ @app.get(
771
+ "/users",
772
+ tags=["users"],
773
+ )
774
+ def list():
775
+ # ...
776
+ ```
777
+
778
+ The generated hooks will be grouped by these tags:
779
+
780
+ ```tsx
781
+ import { useQuery, useMutation } from '@tanstack/react-query';
782
+ import { useMyApi } from './hooks/useMyApi';
783
+
784
+ function ItemsAndUsers() {
785
+ const api = useMyApi();
786
+
787
+ // Items operations are grouped under api.items
788
+ const items = useQuery(api.items.list.queryOptions());
789
+ const createItem = useMutation(api.items.create.mutationOptions());
790
+
791
+ // Users operations are grouped under api.users
792
+ const users = useQuery(api.users.list.queryOptions());
793
+
794
+ // Usage example
795
+ const handleCreateItem = () => {
796
+ createItem.mutate({ name: 'New Item' });
797
+ };
798
+
799
+ return (
800
+ <div>
801
+ <h2>Items</h2>
802
+ <ul>
803
+ {items.data?.map(item => (
804
+ <li key={item.id}>{item.name}</li>
805
+ ))}
806
+ </ul>
807
+ <button onClick={handleCreateItem}>Add Item</button>
808
+
809
+ <h2>Users</h2>
810
+ <ul>
811
+ {users.data?.map(user => (
812
+ <li key={user.id}>{user.name}</li>
813
+ ))}
814
+ </ul>
815
+ </div>
816
+ );
817
+ }
818
+ ```
819
+
820
+ This grouping makes it easier to organize your API calls and provides better code completion in your IDE.
821
+
822
+ <Drawer title="Grouped operations using the API client directly" trigger="Click here for an example using the client directly.">
823
+ ```tsx
824
+ import { useState, useEffect } from 'react';
825
+ import { useMyApiClient } from './hooks/useMyApiClient';
826
+
827
+ function ItemsAndUsers() {
828
+ const api = useMyApiClient();
829
+ const [items, setItems] = useState([]);
830
+ const [users, setUsers] = useState([]);
831
+ const [isLoading, setIsLoading] = useState(true);
832
+
833
+ // Load data
834
+ useEffect(() => {
835
+ const fetchData = async () => {
836
+ try {
837
+ setIsLoading(true);
838
+
839
+ // Items operations are grouped under api.items
840
+ const itemsData = await api.items.list();
841
+ setItems(itemsData);
842
+
843
+ // Users operations are grouped under api.users
844
+ const usersData = await api.users.list();
845
+ setUsers(usersData);
846
+ } catch (error) {
847
+ console.error('Error fetching data:', error);
848
+ } finally {
849
+ setIsLoading(false);
850
+ }
851
+ };
852
+
853
+ fetchData();
854
+ }, [api]);
855
+
856
+ const handleCreateItem = async () => {
857
+ try {
858
+ // Create item using the grouped method
859
+ const newItem = await api.items.create({ name: 'New Item' });
860
+ setItems(prevItems => [...prevItems, newItem]);
861
+ } catch (error) {
862
+ console.error('Error creating item:', error);
863
+ }
864
+ };
865
+
866
+ if (isLoading) {
867
+ return <div>Loading...</div>;
868
+ }
869
+
870
+ return (
871
+ <div>
872
+ <h2>Items</h2>
873
+ <ul>
874
+ {items.map(item => (
875
+ <li key={item.id}>{item.name}</li>
876
+ ))}
877
+ </ul>
878
+ <button onClick={handleCreateItem}>Add Item</button>
879
+
880
+ <h2>Users</h2>
881
+ <ul>
882
+ {users.map(user => (
883
+ <li key={user.id}>{user.name}</li>
884
+ ))}
885
+ </ul>
886
+ </div>
887
+ );
888
+ }
889
+ ```
890
+ </Drawer>
891
+
892
+ :::tip[Splitting Your API]
893
+ You can also split your API using multiple `routers`. See the [FastAPI Documentation](https://fastapi.tiangolo.com/tutorial/bigger-applications/) for more details.
894
+ :::
895
+
896
+ ### Errors
897
+
898
+ You can customize error responses in your FastAPI by defining custom exception classes, exception handlers, and specifying response models for different error status codes. The generated client will automatically handle these custom error types.
899
+
900
+ #### Defining Custom Error Models
901
+
902
+ First, define your error models using Pydantic:
903
+
904
+ ```python title="models.py"
905
+ from pydantic import BaseModel
906
+
907
+ class ErrorDetails(BaseModel):
908
+ message: str
909
+
910
+ class ValidationError(BaseModel):
911
+ message: str
912
+ field_errors: list[str]
913
+ ```
914
+
915
+ #### Creating Custom Exceptions
916
+
917
+ Then create custom exception classes for different error scenarios:
918
+
919
+ ```python title="exceptions.py"
920
+ class NotFoundException(Exception):
921
+ def __init__(self, message: str):
922
+ self.message = message
923
+
924
+ class ValidationException(Exception):
925
+ def __init__(self, details: ValidationError):
926
+ self.details = details
927
+ ```
928
+
929
+ #### Adding Exception Handlers
930
+
931
+ Register exception handlers to convert your exceptions to HTTP responses:
932
+
933
+ ```python title="main.py"
934
+ from fastapi import Request
935
+ from fastapi.responses import JSONResponse
936
+
937
+ @app.exception_handler(NotFoundException)
938
+ async def not_found_handler(request: Request, exc: NotFoundException):
939
+ return JSONResponse(
940
+ status_code=404,
941
+ content=exc.message,
942
+ )
943
+
944
+ @app.exception_handler(ValidationException)
945
+ async def validation_error_handler(request: Request, exc: ValidationException):
946
+ return JSONResponse(
947
+ status_code=400,
948
+ content=exc.details.model_dump(),
949
+ )
950
+ ```
951
+
952
+ :::tip[Custom Response Types]
953
+ The `JSONResponse` accepts a dictionary, so we use our Pydantic model's `model_dump` method.
954
+ :::
955
+
956
+ #### Specifying Response Models
957
+
958
+ Finally, specify the response models for different error status codes in your endpoint definitions:
959
+
960
+ ```python title="main.py"
961
+ @app.get(
962
+ "/items/{item_id}",
963
+ responses={
964
+ 404: {"model": str}
965
+ 500: {"model": ErrorDetails}
966
+ }
967
+ )
968
+ def get_item(item_id: str) -> Item:
969
+ item = find_item(item_id)
970
+ if not item:
971
+ raise NotFoundException(message=f"Item with ID {item_id} not found")
972
+ return item
973
+
974
+ @app.post(
975
+ "/items",
976
+ responses={
977
+ 400: {"model": ValidationError},
978
+ 403: {"model": str}
979
+ }
980
+ )
981
+ def create_item(item: Item) -> Item:
982
+ if not is_valid(item):
983
+ raise ValidationException(
984
+ ValidationError(
985
+ message="Invalid item data",
986
+ field_errors=["name is required"]
987
+ )
988
+ )
989
+ return save_item(item)
990
+ ```
991
+
992
+ #### Using Custom Error Types in React
993
+
994
+ The generated client will automatically handle these custom error types, allowing you to type-check and handle different error responses:
995
+
996
+ ```tsx
997
+ import { useMutation, useQuery } from '@tanstack/react-query';
998
+
999
+ function ItemComponent() {
1000
+ const api = useMyApi();
1001
+
1002
+ // Query with typed error handling
1003
+ const getItem = useQuery({
1004
+ ...api.getItem.queryOptions({ itemId: '123' }),
1005
+ onError: (error) => {
1006
+ // Error is typed based on the responses in your FastAPI
1007
+ switch (error.status) {
1008
+ case 404:
1009
+ // error.error is a string as specified in the responses
1010
+ console.error('Not found:', error.error);
1011
+ break;
1012
+ case 500:
1013
+ // error.error is typed as ErrorDetails
1014
+ console.error('Server error:', error.error.message);
1015
+ break;
1016
+ }
1017
+ }
1018
+ });
1019
+
1020
+ // Mutation with typed error handling
1021
+ const createItem = useMutation({
1022
+ ...api.createItem.mutationOptions(),
1023
+ onError: (error) => {
1024
+ switch (error.status) {
1025
+ case 400:
1026
+ // error.error is typed as ValidationError
1027
+ console.error('Validation error:', error.error.message);
1028
+ console.error('Field errors:', error.error.field_errors);
1029
+ break;
1030
+ case 403:
1031
+ // error.error is a string as specified in the responses
1032
+ console.error('Forbidden:', error.error);
1033
+ break;
1034
+ }
1035
+ }
1036
+ });
1037
+
1038
+ // Component rendering with error handling
1039
+ if (getItem.isError) {
1040
+ if (getItem.error.status === 404) {
1041
+ return <NotFoundMessage message={getItem.error.error} />;
1042
+ } else {
1043
+ return <ErrorMessage message={getItem.error.error.message} />;
1044
+ }
1045
+ }
1046
+
1047
+ return (
1048
+ <div>
1049
+ {/* Component content */}
1050
+ </div>
1051
+ );
1052
+ }
1053
+ ```
1054
+
1055
+ <Drawer title="Handling custom errors with the client directly" trigger="Click here for an example using the client directly.">
1056
+ ```tsx
1057
+ import { useState, useEffect } from 'react';
1058
+
1059
+ function ItemComponent() {
1060
+ const api = useMyApiClient();
1061
+ const [item, setItem] = useState(null);
1062
+ const [error, setError] = useState(null);
1063
+ const [loading, setLoading] = useState(true);
1064
+
1065
+ // Fetch item with error handling
1066
+ useEffect(() => {
1067
+ const fetchItem = async () => {
1068
+ try {
1069
+ setLoading(true);
1070
+ const data = await api.getItem({ itemId: '123' });
1071
+ setItem(data);
1072
+ } catch (e) {
1073
+ // Error is typed based on the responses in your FastAPI
1074
+ const err = e as GetItemError;
1075
+ setError(err);
1076
+
1077
+ switch (err.status) {
1078
+ case 404:
1079
+ // err.error is a string as specified in the responses
1080
+ console.error('Not found:', err.error);
1081
+ break;
1082
+ case 500:
1083
+ // err.error is typed as ErrorDetails
1084
+ console.error('Server error:', err.error.message);
1085
+ break;
1086
+ }
1087
+ } finally {
1088
+ setLoading(false);
1089
+ }
1090
+ };
1091
+
1092
+ fetchItem();
1093
+ }, [api]);
1094
+
1095
+ // Create item with error handling
1096
+ const handleCreateItem = async (data) => {
1097
+ try {
1098
+ await api.createItem(data);
1099
+ } catch (e) {
1100
+ const err = e as CreateItemError;
1101
+
1102
+ switch (err.status) {
1103
+ case 400:
1104
+ // err.error is typed as ValidationError
1105
+ console.error('Validation error:', err.error.message);
1106
+ console.error('Field errors:', err.error.field_errors);
1107
+ break;
1108
+ case 403:
1109
+ // err.error is a string as specified in the responses
1110
+ console.error('Forbidden:', err.error);
1111
+ break;
1112
+ }
1113
+ }
1114
+ };
1115
+
1116
+ // Component rendering with error handling
1117
+ if (loading) {
1118
+ return <LoadingSpinner />;
1119
+ }
1120
+
1121
+ if (error) {
1122
+ if (error.status === 404) {
1123
+ return <NotFoundMessage message={error.error} />;
1124
+ } else if (error.status === 500) {
1125
+ return <ErrorMessage message={error.error.message} />;
1126
+ }
1127
+ }
1128
+
1129
+ return (
1130
+ <div>
1131
+ {/* Component content */}
1132
+ </div>
1133
+ );
1134
+ }
1135
+ ```
1136
+ </Drawer>
1137
+
1138
+ :::tip[Error Responses]
1139
+ When defining error responses in FastAPI, always use the `responses` parameter to specify the model for each status code. This ensures that the generated client will have proper type information for error handling.
1140
+ :::
1141
+
1142
+ ## Best Practices
1143
+
1144
+ ### Handle Loading States
1145
+
1146
+ Always handle loading and error states for a better user experience:
1147
+
1148
+ ```tsx
1149
+ import { useQuery } from '@tanstack/react-query';
1150
+
1151
+ function ItemList() {
1152
+ const api = useMyApi();
1153
+ const items = useQuery(api.listItems.queryOptions());
1154
+
1155
+ if (items.isLoading) {
1156
+ return <LoadingSpinner />;
1157
+ }
1158
+
1159
+ if (items.isError) {
1160
+ const err = items.error;
1161
+ switch (err.status) {
1162
+ case 403:
1163
+ // err.error is typed as ListItems403Response
1164
+ return <ErrorMessage message={err.error.reason} />;
1165
+ case 500:
1166
+ case 502:
1167
+ // err.error is typed as ListItems5XXResponse
1168
+ return (
1169
+ <ErrorMessage
1170
+ message={err.error.message}
1171
+ details={`Trace ID: ${err.error.traceId}`}
1172
+ />
1173
+ );
1174
+ default:
1175
+ return <ErrorMessage message="An unknown error occurred" />;
1176
+ }
1177
+ }
1178
+
1179
+ return (
1180
+ <ul>
1181
+ {items.data.map((item) => (
1182
+ <li key={item.id}>{item.name}</li>
1183
+ ))}
1184
+ </ul>
1185
+ );
1186
+ }
1187
+ ```
1188
+
1189
+ <Drawer title="Handle loading states using the API client directly" trigger="Click here for an example using the vanilla client directly.">
1190
+ ```tsx
1191
+ function ItemList() {
1192
+ const api = useMyApiClient();
1193
+ const [items, setItems] = useState([]);
1194
+ const [loading, setLoading] = useState(true);
1195
+ const [error, setError] = useState(null);
1196
+
1197
+ useEffect(() => {
1198
+ const fetchItems = async () => {
1199
+ try {
1200
+ const data = await api.listItems();
1201
+ setItems(data);
1202
+ } catch (err) {
1203
+ setError(err);
1204
+ } finally {
1205
+ setLoading(false);
1206
+ }
1207
+ };
1208
+ fetchItems();
1209
+ }, [api]);
1210
+
1211
+ if (loading) {
1212
+ return <LoadingSpinner />;
1213
+ }
1214
+
1215
+ if (error) {
1216
+ const err = error as ListItemsError;
1217
+ switch (err.status) {
1218
+ case 403:
1219
+ // err.error is typed as ListItems403Response
1220
+ return <ErrorMessage message={err.error.reason} />;
1221
+ case 500:
1222
+ case 502:
1223
+ // err.error is typed as ListItems5XXResponse
1224
+ return (
1225
+ <ErrorMessage
1226
+ message={err.error.message}
1227
+ details={`Trace ID: ${err.error.traceId}`}
1228
+ />
1229
+ );
1230
+ default:
1231
+ return <ErrorMessage message="An unknown error occurred" />;
1232
+ }
1233
+ }
1234
+
1235
+ return (
1236
+ <ul>
1237
+ {items.map((item) => (
1238
+ <li key={item.id}>{item.name}</li>
1239
+ ))}
1240
+ </ul>
1241
+ );
1242
+ }
1243
+ ```
1244
+ </Drawer>
1245
+
1246
+ ### Optimistic Updates
1247
+
1248
+ Implement optimistic updates for a better user experience:
1249
+
1250
+ ```tsx
1251
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
1252
+
1253
+ function ItemList() {
1254
+ const api = useMyApi();
1255
+ const queryClient = useQueryClient();
1256
+
1257
+ // Query to fetch items
1258
+ const itemsQuery = useQuery(api.listItems.queryOptions());
1259
+
1260
+ // Mutation for deleting items with optimistic updates
1261
+ const deleteMutation = useMutation({
1262
+ ...api.deleteItem.mutationOptions(),
1263
+ onMutate: async (itemId) => {
1264
+ // Cancel any outgoing refetches
1265
+ await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
1266
+
1267
+ // Snapshot the previous value
1268
+ const previousItems = queryClient.getQueryData(api.listItems.queryKey());
1269
+
1270
+ // Optimistically update to the new value
1271
+ queryClient.setQueryData(
1272
+ api.listItems.queryKey(),
1273
+ (old) => old.filter((item) => item.id !== itemId)
1274
+ );
1275
+
1276
+ // Return a context object with the snapshot
1277
+ return { previousItems };
1278
+ },
1279
+ onError: (err, itemId, context) => {
1280
+ // If the mutation fails, use the context returned from onMutate to roll back
1281
+ queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
1282
+ console.error('Failed to delete item:', err);
1283
+ },
1284
+ onSettled: () => {
1285
+ // Always refetch after error or success to ensure data is in sync with server
1286
+ queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
1287
+ },
1288
+ });
1289
+
1290
+ if (itemsQuery.isLoading) {
1291
+ return <LoadingSpinner />;
1292
+ }
1293
+
1294
+ if (itemsQuery.isError) {
1295
+ return <ErrorMessage message="Failed to load items" />;
1296
+ }
1297
+
1298
+ return (
1299
+ <ul>
1300
+ {itemsQuery.data.map((item) => (
1301
+ <li key={item.id}>
1302
+ {item.name}
1303
+ <button
1304
+ onClick={() => deleteMutation.mutate(item.id)}
1305
+ disabled={deleteMutation.isPending}
1306
+ >
1307
+ {deleteMutation.isPending ? 'Deleting...' : 'Delete'}
1308
+ </button>
1309
+ </li>
1310
+ ))}
1311
+ </ul>
1312
+ );
1313
+ }
1314
+ ```
1315
+
1316
+ <Drawer title="Optimistic updates using the API client directly" trigger="Click here for an example using the vanilla client directly.">
1317
+ ```tsx
1318
+ function ItemList() {
1319
+ const api = useMyApiClient();
1320
+ const [items, setItems] = useState([]);
1321
+
1322
+ const handleDelete = async (itemId) => {
1323
+ // Optimistically remove the item
1324
+ const previousItems = items;
1325
+ setItems(items.filter((item) => item.id !== itemId));
1326
+
1327
+ try {
1328
+ await api.deleteItem(itemId);
1329
+ } catch (error) {
1330
+ // Restore previous items on error
1331
+ setItems(previousItems);
1332
+ console.error('Failed to delete item:', error);
1333
+ }
1334
+ };
1335
+
1336
+ return (
1337
+ <ul>
1338
+ {items.map((item) => (
1339
+ <li key={item.id}>
1340
+ {item.name}
1341
+ <button onClick={() => handleDelete(item.id)}>Delete</button>
1342
+ </li>
1343
+ ))}
1344
+ </ul>
1345
+ );
1346
+ }
1347
+ ```
1348
+ </Drawer>
1349
+
1350
+ ## Type Safety
1351
+
1352
+ The integration provides complete end-to-end type safety. Your IDE will provide full autocompletion and type checking for all your API calls:
1353
+
1354
+ ```tsx
1355
+ import { useMutation } from '@tanstack/react-query';
1356
+
1357
+ function ItemForm() {
1358
+ const api = useMyApi();
1359
+
1360
+ // Type-safe mutation for creating items
1361
+ const createItem = useMutation({
1362
+ ...api.createItem.mutationOptions(),
1363
+ // ✅ Type error if onSuccess callback doesn't handle the correct response type
1364
+ onSuccess: (data) => {
1365
+ // data is fully typed based on your API's response schema
1366
+ console.log(`Item created with ID: ${data.id}`);
1367
+ },
1368
+ });
1369
+
1370
+ const handleSubmit = (data: CreateItemInput) => {
1371
+ // ✅ Type error if input doesn't match schema
1372
+ createItem.mutate(data);
1373
+ };
1374
+
1375
+ // Error UI can use type narrowing to handle different error types
1376
+ if (createItem.error) {
1377
+ const error = createItem.error;
1378
+ switch (error.status) {
1379
+ case 400:
1380
+ // error.error is typed as CreateItem400Response
1381
+ return (
1382
+ <FormError
1383
+ message="Invalid input"
1384
+ errors={error.error.validationErrors}
1385
+ />
1386
+ );
1387
+ case 403:
1388
+ // error.error is typed as CreateItem403Response
1389
+ return <AuthError reason={error.error.reason} />;
1390
+ default:
1391
+ // error.error is typed as CreateItem5XXResponse for 500, 502, etc.
1392
+ return <ServerError message={error.error.message} />;
1393
+ }
1394
+ }
1395
+
1396
+ return (
1397
+ <form onSubmit={(e) => {
1398
+ e.preventDefault();
1399
+ handleSubmit({ name: 'New Item' });
1400
+ }}>
1401
+ {/* Form fields */}
1402
+ <button
1403
+ type="submit"
1404
+ disabled={createItem.isPending}
1405
+ >
1406
+ {createItem.isPending ? 'Creating...' : 'Create Item'}
1407
+ </button>
1408
+ </form>
1409
+ );
1410
+ }
1411
+ ```
1412
+
1413
+ <Drawer title="Type safety using the API client directly" trigger="Click here for an example using the vanilla client directly.">
1414
+ ```tsx
1415
+ function ItemForm() {
1416
+ const api = useMyApiClient();
1417
+ const [error, setError] = useState<CreateItemError | null>(null);
1418
+
1419
+ const handleSubmit = async (data: CreateItemInput) => {
1420
+ try {
1421
+ // ✅ Type error if input doesn't match schema
1422
+ await api.createItem(data);
1423
+ } catch (e) {
1424
+ // ✅ Error type includes all possible error responses
1425
+ const err = e as CreateItemError;
1426
+ switch (err.status) {
1427
+ case 400:
1428
+ // err.error is typed as CreateItem400Response
1429
+ console.error('Validation errors:', err.error.validationErrors);
1430
+ break;
1431
+ case 403:
1432
+ // err.error is typed as CreateItem403Response
1433
+ console.error('Not authorized:', err.error.reason);
1434
+ break;
1435
+ case 500:
1436
+ case 502:
1437
+ // err.error is typed as CreateItem5XXResponse
1438
+ console.error(
1439
+ 'Server error:',
1440
+ err.error.message,
1441
+ 'Trace:',
1442
+ err.error.traceId,
1443
+ );
1444
+ break;
1445
+ }
1446
+ setError(err);
1447
+ }
1448
+ };
1449
+
1450
+ // Error UI can use type narrowing to handle different error types
1451
+ if (error) {
1452
+ switch (error.status) {
1453
+ case 400:
1454
+ return (
1455
+ <FormError
1456
+ message="Invalid input"
1457
+ errors={error.error.validationErrors}
1458
+ />
1459
+ );
1460
+ case 403:
1461
+ return <AuthError reason={error.error.reason} />;
1462
+ default:
1463
+ return <ServerError message={error.error.message} />;
1464
+ }
1465
+ }
1466
+
1467
+ return <form onSubmit={handleSubmit}>{/* ... */}</form>;
1468
+ }
1469
+ ```
1470
+ </Drawer>
1471
+
1472
+ The types are automatically generated from your FastAPI's OpenAPI schema, ensuring that any changes to your API are reflected in your frontend code after a build.
1473
+
1474
+ ## Custom Auth
1475
+
1476
+ If your FastAPI uses `Custom` authentication (Lambda Authorizer), you will need to edit the generated client provider to add the authorization headers your authorizer expects. Look for the `fetch` configuration in the generated `<ApiName>Provider.tsx` and add your token or API key to the request headers.