@aws/nx-plugin-mcp 0.120.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.
- package/bin/aws-nx-mcp.js +23 -35
- package/docs/guides/astro-docs.mdx +142 -0
- package/docs/guides/connection/py-agent-a2a.mdx +105 -0
- package/docs/guides/connection/py-agent-mcp.mdx +147 -0
- package/docs/guides/connection/react-agui.mdx +240 -0
- package/docs/guides/connection/react-fastapi.mdx +1476 -0
- package/docs/guides/connection/react-py-agent.mdx +198 -0
- package/docs/guides/connection/react-smithy.mdx +1355 -0
- package/docs/guides/connection/react-trpc.mdx +447 -0
- package/docs/guides/connection/react-ts-agent.mdx +198 -0
- package/docs/guides/connection/smithy-rdb.mdx +161 -0
- package/docs/guides/connection/trpc-rdb.mdx +127 -0
- package/docs/guides/connection/ts-agent-a2a.mdx +106 -0
- package/docs/guides/connection/ts-agent-mcp.mdx +144 -0
- package/docs/guides/connection/ts-agent-rdb.mdx +141 -0
- package/docs/guides/connection/ts-mcp-server-rdb.mdx +135 -0
- package/docs/guides/connection.mdx +126 -0
- package/docs/guides/docker-bundling.mdx +403 -0
- package/docs/guides/fastapi.mdx +509 -0
- package/docs/guides/license.mdx +293 -0
- package/docs/guides/nx-generator.mdx +588 -0
- package/docs/guides/py-agent.mdx +483 -0
- package/docs/guides/py-mcp-server.mdx +161 -0
- package/docs/guides/python-lambda-function.mdx +207 -0
- package/docs/guides/python-project.mdx +228 -0
- package/docs/guides/react-website-auth.mdx +216 -0
- package/docs/guides/react-website.mdx +421 -0
- package/docs/guides/runtime-config.mdx +312 -0
- package/docs/guides/terraform-project.mdx +317 -0
- package/docs/guides/trpc.mdx +816 -0
- package/docs/guides/ts-agent.mdx +436 -0
- package/docs/guides/ts-lambda-function.mdx +218 -0
- package/docs/guides/ts-mcp-server.mdx +169 -0
- package/docs/guides/ts-nx-plugin.mdx +159 -0
- package/docs/guides/ts-rdb.mdx +759 -0
- package/docs/guides/ts-smithy-api.mdx +661 -0
- package/docs/guides/typescript-infrastructure.mdx +408 -0
- package/docs/guides/typescript-project.mdx +312 -0
- package/docs/guides/workspace.mdx +181 -0
- package/docs/snippets/agent/architecture.mdx +72 -0
- package/docs/snippets/agent/bedrock-deployment.mdx +172 -0
- package/docs/snippets/agent/runtime-arn.mdx +64 -0
- package/docs/snippets/api/api-architecture.mdx +93 -0
- package/docs/snippets/api/api-choice-note.mdx +6 -0
- package/docs/snippets/api/cors-configuration-cdk-note.mdx +25 -0
- package/docs/snippets/api/cors-configuration-terraform-note.mdx +28 -0
- package/docs/snippets/api/shared-constructs.mdx +38 -0
- package/docs/snippets/api/type-safe-api-integrations.mdx +643 -0
- package/docs/snippets/api/waf-configuration.mdx +37 -0
- package/docs/snippets/connection/a2a-infrastructure.mdx +63 -0
- package/docs/snippets/connection/lambda-rdb-ssl-requirements.mdx +40 -0
- package/docs/snippets/connection/mcp-server-rdb-ssl-requirements.mdx +35 -0
- package/docs/snippets/connection/rdb-api-infrastructure.mdx +72 -0
- package/docs/snippets/connection/react-agent-infrastructure.mdx +61 -0
- package/docs/snippets/connection/strands-agent-rdb-ssl-requirements.mdx +35 -0
- package/docs/snippets/lambda-function/architecture.mdx +36 -0
- package/docs/snippets/lambda-function/deploying-your-function.mdx +118 -0
- package/docs/snippets/mcp/architecture.mdx +58 -0
- package/docs/snippets/mcp/assistant-docs.mdx +10 -0
- package/docs/snippets/mcp/bedrock-deployment.mdx +167 -0
- package/docs/snippets/mcp/config.mdx +13 -0
- package/docs/snippets/mcp/configuration-py.mdx +42 -0
- package/docs/snippets/mcp/configuration-ts.mdx +53 -0
- package/docs/snippets/mcp/observability.mdx +8 -0
- package/docs/snippets/mcp/shared-constructs.mdx +32 -0
- package/docs/snippets/pdk-migration/example/01-migrate-api.mdx +602 -0
- package/docs/snippets/pdk-migration/example/02-migrate-website.mdx +915 -0
- package/docs/snippets/pdk-migration/example/03-migrate-infra.mdx +161 -0
- package/docs/snippets/pdk-migration/example/04-deploy.mdx +229 -0
- package/docs/snippets/pdk-migration/faq/aws-arch.mdx +17 -0
- package/docs/snippets/pdk-migration/faq/cdk-graph.mdx +29 -0
- package/docs/snippets/pdk-migration/faq/infrastructure-python-java.mdx +19 -0
- package/docs/snippets/pdk-migration/faq/pdk-nag.mdx +15 -0
- package/docs/snippets/pdk-migration/faq/pipeline.mdx +15 -0
- package/docs/snippets/pdk-migration/faq/type-safe-api.mdx +310 -0
- package/docs/snippets/pdk-migration/faq/use-of-projen.mdx +15 -0
- package/docs/snippets/prerequisites.mdx +20 -0
- package/docs/snippets/required-prerequisites.mdx +12 -0
- package/docs/snippets/shared-constructs.mdx +40 -0
- package/docs/snippets/tools/acurl.mdx +73 -0
- package/docs/snippets/ts-bundle.mdx +14 -0
- package/package.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.
|