@aws/nx-plugin-mcp 0.120.0 → 0.122.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 +2365 -2078
- 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,1355 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: React to Smithy API
|
|
3
|
+
description: Connect a React website to a Smithy TypeScript API
|
|
4
|
+
when:
|
|
5
|
+
sourceType: react
|
|
6
|
+
targetType: smithy
|
|
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 Smithy TypeScript API backend. It sets up all necessary configuration for connecting to your Smithy API 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 Smithy TypeScript API backend (generated using the <Link path="/guides/ts-smithy-api">`ts#smithy-api` generator</Link>)
|
|
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 React application:
|
|
60
|
+
|
|
61
|
+
<FileTree>
|
|
62
|
+
|
|
63
|
+
- src
|
|
64
|
+
- components
|
|
65
|
+
- \<ApiName>Provider.tsx Provider for your API client
|
|
66
|
+
- QueryClientProvider.tsx TanStack React Query client provider
|
|
67
|
+
- RuntimeConfig/ Runtime configuration component for local development
|
|
68
|
+
- hooks
|
|
69
|
+
- use\<ApiName>.tsx Add a hook for calling your API with state managed by TanStack Query
|
|
70
|
+
- use\<ApiName>Client.tsx Add a hook for instantiating the vanilla API client which can call your API.
|
|
71
|
+
- useSigV4.tsx Add a hook for signing HTTP requests with SigV4 (if you selected IAM authentication)
|
|
72
|
+
- project.json A new target is added to the build which generates a type-safe client
|
|
73
|
+
- .gitignore The generated client files are ignored by default
|
|
74
|
+
|
|
75
|
+
</FileTree>
|
|
76
|
+
|
|
77
|
+
The generator will also add a file to your Smithy model:
|
|
78
|
+
|
|
79
|
+
<FileTree>
|
|
80
|
+
|
|
81
|
+
- model
|
|
82
|
+
- src
|
|
83
|
+
- extensions.smithy Defines traits which can be used to customise the generated client
|
|
84
|
+
|
|
85
|
+
</FileTree>
|
|
86
|
+
|
|
87
|
+
The generator will also add Runtime Config to your website infrastructure if not present already, which ensures that the API URL for your Smithy API is available in the website and automatically configured by the `use<ApiName>.tsx` hook.
|
|
88
|
+
|
|
89
|
+
### Code Generation
|
|
90
|
+
|
|
91
|
+
At build time, a type-safe client is generated from your Smithy API's OpenAPI specification. This will add three new files to your React application:
|
|
92
|
+
|
|
93
|
+
<FileTree>
|
|
94
|
+
|
|
95
|
+
- src
|
|
96
|
+
- generated
|
|
97
|
+
- \<ApiName>
|
|
98
|
+
- types.gen.ts Generated types from the Smithy model structures
|
|
99
|
+
- client.gen.ts Type-safe client for calling your API
|
|
100
|
+
- options-proxy.gen.ts Provides methods to create TanStack Query hooks options for interacting with your API using TanStack Query
|
|
101
|
+
|
|
102
|
+
</FileTree>
|
|
103
|
+
|
|
104
|
+
:::tip[Generated Client in Git]
|
|
105
|
+
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.
|
|
106
|
+
:::
|
|
107
|
+
|
|
108
|
+
## Using the Generated Code
|
|
109
|
+
|
|
110
|
+
The generated type-safe client can be used to call your Smithy API 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.
|
|
111
|
+
|
|
112
|
+
:::note[Regenerating Client Code]
|
|
113
|
+
Whenever you make changes to your Smithy API model, you need to rebuild your project in order for those changes to be reflected in the generated client. For example:
|
|
114
|
+
|
|
115
|
+
<PackageManagerShortCommand commands={["build"]} />
|
|
116
|
+
:::
|
|
117
|
+
|
|
118
|
+
:::tip[Auto-Regeneration]
|
|
119
|
+
If you're actively working on both your React application and Smithy API 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 Smithy API server:
|
|
120
|
+
|
|
121
|
+
<NxCommands commands={['serve-local <WebsiteProject>']} />
|
|
122
|
+
|
|
123
|
+
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:
|
|
124
|
+
|
|
125
|
+
<NxCommands commands={['run <WebsiteProject>:"watch-generate:<ApiName>-client"']}
|
|
126
|
+
/>
|
|
127
|
+
:::
|
|
128
|
+
|
|
129
|
+
### Using the API Hook
|
|
130
|
+
|
|
131
|
+
The generator provides a `use<ApiName>` hook which you can use to call your API with TanStack Query.
|
|
132
|
+
|
|
133
|
+
### Queries
|
|
134
|
+
|
|
135
|
+
You can use the `queryOptions` method to retrieve the options required for calling your API using TanStack Query's `useQuery` hook:
|
|
136
|
+
|
|
137
|
+
```tsx {7}
|
|
138
|
+
import { useQuery } from '@tanstack/react-query';
|
|
139
|
+
import { useState, useEffect } from 'react';
|
|
140
|
+
import { useMyApi } from './hooks/useMyApi';
|
|
141
|
+
|
|
142
|
+
function MyComponent() {
|
|
143
|
+
const api = useMyApi();
|
|
144
|
+
const item = useQuery(api.getItem.queryOptions({ itemId: 'some-id' }));
|
|
145
|
+
|
|
146
|
+
if (item.isLoading) return <div>Loading...</div>;
|
|
147
|
+
if (item.isError) return <div>Error: {item.error.message}</div>;
|
|
148
|
+
|
|
149
|
+
return <div>Item: {item.data.name}</div>;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
<Drawer title="Using the API client directly" trigger="Click here for an example using the vanilla client directly.">
|
|
154
|
+
```tsx {5,13}
|
|
155
|
+
import { useState, useEffect } from 'react';
|
|
156
|
+
import { useMyApiClient } from './hooks/useMyApiClient';
|
|
157
|
+
|
|
158
|
+
function MyComponent() {
|
|
159
|
+
const api = useMyApiClient();
|
|
160
|
+
const [item, setItem] = useState(null);
|
|
161
|
+
const [loading, setLoading] = useState(true);
|
|
162
|
+
const [error, setError] = useState(null);
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const fetchItem = async () => {
|
|
166
|
+
try {
|
|
167
|
+
const data = await api.getItem({ itemId: 'some-id' });
|
|
168
|
+
setItem(data);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
setError(err);
|
|
171
|
+
} finally {
|
|
172
|
+
setLoading(false);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
fetchItem();
|
|
176
|
+
}, [api]);
|
|
177
|
+
|
|
178
|
+
if (loading) return <div>Loading...</div>;
|
|
179
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
180
|
+
|
|
181
|
+
return <div>Item: {item.name}</div>;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
</Drawer>
|
|
185
|
+
|
|
186
|
+
### Mutations
|
|
187
|
+
|
|
188
|
+
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.
|
|
189
|
+
|
|
190
|
+
```tsx {5-7,11}
|
|
191
|
+
import { useMutation } from '@tanstack/react-query';
|
|
192
|
+
import { useMyApi } from './hooks/useMyApi';
|
|
193
|
+
|
|
194
|
+
function CreateItemForm() {
|
|
195
|
+
const api = useMyApi();
|
|
196
|
+
// Create a mutation using the generated mutation options
|
|
197
|
+
const createItem = useMutation(api.createItem.mutationOptions());
|
|
198
|
+
|
|
199
|
+
const handleSubmit = (e) => {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
createItem.mutate({ name: 'New Item', description: 'A new item' });
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<form onSubmit={handleSubmit}>
|
|
206
|
+
{/* Form fields */}
|
|
207
|
+
<button
|
|
208
|
+
type="submit"
|
|
209
|
+
disabled={createItem.isPending}
|
|
210
|
+
>
|
|
211
|
+
{createItem.isPending ? 'Creating...' : 'Create Item'}
|
|
212
|
+
</button>
|
|
213
|
+
|
|
214
|
+
{createItem.isSuccess && (
|
|
215
|
+
<div className="success">
|
|
216
|
+
Item created with ID: {createItem.data.id}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{createItem.isError && (
|
|
221
|
+
<div className="error">
|
|
222
|
+
Error: {createItem.error.message}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</form>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
You can also add callbacks for different mutation states:
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
const createItem = useMutation({
|
|
234
|
+
...api.createItem.mutationOptions(),
|
|
235
|
+
onSuccess: (data) => {
|
|
236
|
+
// This will run when the mutation succeeds
|
|
237
|
+
console.log('Item created:', data);
|
|
238
|
+
// You can navigate to the new item
|
|
239
|
+
navigate(`/items/${data.id}`);
|
|
240
|
+
},
|
|
241
|
+
onError: (error) => {
|
|
242
|
+
// This will run when the mutation fails
|
|
243
|
+
console.error('Failed to create item:', error);
|
|
244
|
+
},
|
|
245
|
+
onSettled: () => {
|
|
246
|
+
// This will run when the mutation completes (success or error)
|
|
247
|
+
// Good place to invalidate queries that might be affected
|
|
248
|
+
queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
<Drawer title="Mutations using the API client directly" trigger="Click here for an example using the client directly.">
|
|
254
|
+
```tsx
|
|
255
|
+
import { useState } from 'react';
|
|
256
|
+
import { useMyApiClient } from './hooks/useMyApiClient';
|
|
257
|
+
|
|
258
|
+
function CreateItemForm() {
|
|
259
|
+
const api = useMyApiClient();
|
|
260
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
261
|
+
const [error, setError] = useState(null);
|
|
262
|
+
const [createdItem, setCreatedItem] = useState(null);
|
|
263
|
+
|
|
264
|
+
const handleSubmit = async (e) => {
|
|
265
|
+
e.preventDefault();
|
|
266
|
+
setIsLoading(true);
|
|
267
|
+
setError(null);
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const newItem = await api.createItem({
|
|
271
|
+
name: 'New Item',
|
|
272
|
+
description: 'A new item'
|
|
273
|
+
});
|
|
274
|
+
setCreatedItem(newItem);
|
|
275
|
+
// You can navigate to the new item
|
|
276
|
+
// navigate(`/items/${newItem.id}`);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
setError(err);
|
|
279
|
+
console.error('Failed to create item:', err);
|
|
280
|
+
} finally {
|
|
281
|
+
setIsLoading(false);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<form onSubmit={handleSubmit}>
|
|
287
|
+
{/* Form fields */}
|
|
288
|
+
<button
|
|
289
|
+
type="submit"
|
|
290
|
+
disabled={isLoading}
|
|
291
|
+
>
|
|
292
|
+
{isLoading ? 'Creating...' : 'Create Item'}
|
|
293
|
+
</button>
|
|
294
|
+
|
|
295
|
+
{createdItem && (
|
|
296
|
+
<div className="success">
|
|
297
|
+
Item created with ID: {createdItem.id}
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
|
|
301
|
+
{error && (
|
|
302
|
+
<div className="error">
|
|
303
|
+
Error: {error.message}
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
</form>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
</Drawer>
|
|
311
|
+
|
|
312
|
+
### Pagination with Infinite Queries
|
|
313
|
+
|
|
314
|
+
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.
|
|
315
|
+
|
|
316
|
+
```tsx {6-16,30-34}
|
|
317
|
+
import { useInfiniteQuery } from '@tanstack/react-query';
|
|
318
|
+
import { useMyApi } from './hooks/useMyApi';
|
|
319
|
+
|
|
320
|
+
function ItemList() {
|
|
321
|
+
const api = useMyApi();
|
|
322
|
+
const items = useInfiniteQuery({
|
|
323
|
+
...api.listItems.infiniteQueryOptions({
|
|
324
|
+
limit: 10, // Number of items per page
|
|
325
|
+
}, {
|
|
326
|
+
// Make sure you define a getNextPageParam function to return
|
|
327
|
+
// the parameter that should be passed as the 'cursor' for the
|
|
328
|
+
// next page
|
|
329
|
+
getNextPageParam: (lastPage) =>
|
|
330
|
+
lastPage.nextCursor || undefined
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (items.isLoading) {
|
|
335
|
+
return <LoadingSpinner />;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (items.isError) {
|
|
339
|
+
return <ErrorMessage message={items.error.message} />;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<div>
|
|
344
|
+
{/* Flatten the pages array to render all items */}
|
|
345
|
+
<ul>
|
|
346
|
+
{items.data.pages.flatMap(page =>
|
|
347
|
+
page.items.map(item => (
|
|
348
|
+
<li key={item.id}>{item.name}</li>
|
|
349
|
+
))
|
|
350
|
+
)}
|
|
351
|
+
</ul>
|
|
352
|
+
|
|
353
|
+
<button
|
|
354
|
+
onClick={() => items.fetchNextPage()}
|
|
355
|
+
disabled={!items.hasNextPage || items.isFetchingNextPage}
|
|
356
|
+
>
|
|
357
|
+
{items.isFetchingNextPage
|
|
358
|
+
? 'Loading more...'
|
|
359
|
+
: items.hasNextPage
|
|
360
|
+
? 'Load More'
|
|
361
|
+
: 'No more items'}
|
|
362
|
+
</button>
|
|
363
|
+
</div>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
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.
|
|
369
|
+
|
|
370
|
+
:::tip[Paginated APIs]
|
|
371
|
+
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).
|
|
372
|
+
:::
|
|
373
|
+
|
|
374
|
+
<Drawer title="Pagination using the API client directly" trigger="Click here for an example using the client directly.">
|
|
375
|
+
```tsx
|
|
376
|
+
import { useState, useEffect } from 'react';
|
|
377
|
+
import { useMyApiClient } from './hooks/useMyApiClient';
|
|
378
|
+
|
|
379
|
+
function ItemList() {
|
|
380
|
+
const api = useMyApiClient();
|
|
381
|
+
const [items, setItems] = useState([]);
|
|
382
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
383
|
+
const [error, setError] = useState(null);
|
|
384
|
+
const [nextCursor, setNextCursor] = useState(null);
|
|
385
|
+
const [isFetchingMore, setIsFetchingMore] = useState(false);
|
|
386
|
+
|
|
387
|
+
// Fetch initial data
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
const fetchItems = async () => {
|
|
390
|
+
try {
|
|
391
|
+
setIsLoading(true);
|
|
392
|
+
const response = await api.listItems({ limit: 10 });
|
|
393
|
+
setItems(response.items);
|
|
394
|
+
setNextCursor(response.nextCursor);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
setError(err);
|
|
397
|
+
} finally {
|
|
398
|
+
setIsLoading(false);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
fetchItems();
|
|
403
|
+
}, [api]);
|
|
404
|
+
|
|
405
|
+
// Function to load more items
|
|
406
|
+
const loadMore = async () => {
|
|
407
|
+
if (!nextCursor) return;
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
setIsFetchingMore(true);
|
|
411
|
+
const response = await api.listItems({
|
|
412
|
+
limit: 10,
|
|
413
|
+
cursor: nextCursor
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
setItems(prevItems => [...prevItems, ...response.items]);
|
|
417
|
+
setNextCursor(response.nextCursor);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
setError(err);
|
|
420
|
+
} finally {
|
|
421
|
+
setIsFetchingMore(false);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
if (isLoading) {
|
|
426
|
+
return <LoadingSpinner />;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (error) {
|
|
430
|
+
return <ErrorMessage message={error.message} />;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return (
|
|
434
|
+
<div>
|
|
435
|
+
<ul>
|
|
436
|
+
{items.map(item => (
|
|
437
|
+
<li key={item.id}>{item.name}</li>
|
|
438
|
+
))}
|
|
439
|
+
</ul>
|
|
440
|
+
|
|
441
|
+
<button
|
|
442
|
+
onClick={loadMore}
|
|
443
|
+
disabled={!nextCursor || isFetchingMore}
|
|
444
|
+
>
|
|
445
|
+
{isFetchingMore
|
|
446
|
+
? 'Loading more...'
|
|
447
|
+
: nextCursor
|
|
448
|
+
? 'Load More'
|
|
449
|
+
: 'No more items'}
|
|
450
|
+
</button>
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
</Drawer>
|
|
456
|
+
|
|
457
|
+
### Error Handling
|
|
458
|
+
|
|
459
|
+
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 Smithy model. Each error has a `status` and `error` property, and by checking the value of `status` you can narrow to a specific type of error.
|
|
460
|
+
|
|
461
|
+
```tsx {12}
|
|
462
|
+
import { useMutation } from '@tanstack/react-query';
|
|
463
|
+
|
|
464
|
+
function MyComponent() {
|
|
465
|
+
const api = useMyApi();
|
|
466
|
+
const createItem = useMutation(api.createItem.mutationOptions());
|
|
467
|
+
|
|
468
|
+
const handleClick = () => {
|
|
469
|
+
createItem.mutate({ name: 'New Item' });
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
if (createItem.error) {
|
|
473
|
+
switch (createItem.error.status) {
|
|
474
|
+
case 400:
|
|
475
|
+
// error.error is typed as CreateItem400Response
|
|
476
|
+
return (
|
|
477
|
+
<div>
|
|
478
|
+
<h2>Invalid input:</h2>
|
|
479
|
+
<p>{createItem.error.error.message}</p>
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
case 403:
|
|
483
|
+
// error.error is typed as CreateItem403Response
|
|
484
|
+
return (
|
|
485
|
+
<div>
|
|
486
|
+
<h2>Not authorized:</h2>
|
|
487
|
+
<p>{createItem.error.error.reason}</p>
|
|
488
|
+
</div>
|
|
489
|
+
);
|
|
490
|
+
case 500:
|
|
491
|
+
case 502:
|
|
492
|
+
// error.error is typed as CreateItem5XXResponse
|
|
493
|
+
return (
|
|
494
|
+
<div>
|
|
495
|
+
<h2>Server error:</h2>
|
|
496
|
+
<p>{createItem.error.error.message}</p>
|
|
497
|
+
</div>
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return <button onClick={handleClick}>Create Item</button>;
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
<Drawer title="Error handling using the API client directly" trigger="Click here for an example using the vanilla client directly.">
|
|
507
|
+
```tsx {9,15}
|
|
508
|
+
function MyComponent() {
|
|
509
|
+
const api = useMyApiClient();
|
|
510
|
+
const [error, setError] = useState<CreateItemError | null>(null);
|
|
511
|
+
|
|
512
|
+
const handleClick = async () => {
|
|
513
|
+
try {
|
|
514
|
+
await api.createItem({ name: 'New Item' });
|
|
515
|
+
} catch (e) {
|
|
516
|
+
const err = e as CreateItemError;
|
|
517
|
+
setError(err);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
if (error) {
|
|
522
|
+
switch (error.status) {
|
|
523
|
+
case 400:
|
|
524
|
+
// error.error is typed as CreateItem400Response
|
|
525
|
+
return (
|
|
526
|
+
<div>
|
|
527
|
+
<h2>Invalid input:</h2>
|
|
528
|
+
<p>{error.error.message}</p>
|
|
529
|
+
</div>
|
|
530
|
+
);
|
|
531
|
+
case 403:
|
|
532
|
+
// error.error is typed as CreateItem403Response
|
|
533
|
+
return (
|
|
534
|
+
<div>
|
|
535
|
+
<h2>Not authorized:</h2>
|
|
536
|
+
<p>{error.error.reason}</p>
|
|
537
|
+
</div>
|
|
538
|
+
);
|
|
539
|
+
case 500:
|
|
540
|
+
case 502:
|
|
541
|
+
// error.error is typed as CreateItem5XXResponse
|
|
542
|
+
return (
|
|
543
|
+
<div>
|
|
544
|
+
<h2>Server error:</h2>
|
|
545
|
+
<p>{error.error.message}</p>
|
|
546
|
+
</div>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return <button onClick={handleClick}>Create Item</button>;
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
</Drawer>
|
|
555
|
+
|
|
556
|
+
## Customising the Generated Code
|
|
557
|
+
|
|
558
|
+
A selection of Smithy traits are added to your target Smithy `model` project in `extensions.smithy` which you can use to customise the generated client.
|
|
559
|
+
|
|
560
|
+
:::tip[Default Operations]
|
|
561
|
+
If you do not need to customise the generated client, you can safely delete `extensions.smithy`
|
|
562
|
+
:::
|
|
563
|
+
|
|
564
|
+
### Queries and Mutations
|
|
565
|
+
|
|
566
|
+
By default, operations in your Smithy API which use the HTTP methods `PUT`, `POST`, `PATCH` and `DELETE` are considered mutations, and all others are considered queries.
|
|
567
|
+
|
|
568
|
+
You can change this behaviour using the `@query` and `@mutation` Smithy traits which are added to your model project in `extensions.smithy`.
|
|
569
|
+
|
|
570
|
+
:::note[OpenAPI Extensions]
|
|
571
|
+
These map to OpenAPI vendor extensions using the [`@specificationExtension` trait](https://smithy.io/2.0/guides/model-translations/converting-to-openapi.html#specificationextension-trait), which our code generator interprets when generating the client from the OpenAPI specification.
|
|
572
|
+
:::
|
|
573
|
+
|
|
574
|
+
#### @query
|
|
575
|
+
|
|
576
|
+
Then Apply the `@query` trait to your Smithy operation to force it to be treated as a query:
|
|
577
|
+
|
|
578
|
+
```smithy
|
|
579
|
+
@http(method: "POST", uri: "/items")
|
|
580
|
+
@query
|
|
581
|
+
operation ListItems {
|
|
582
|
+
input: ListItemsInput
|
|
583
|
+
output: ListItemsOutput
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
The generated hook will provide `queryOptions` even though it uses the `POST` HTTP method:
|
|
588
|
+
|
|
589
|
+
```tsx
|
|
590
|
+
const items = useQuery(api.listItems.queryOptions());
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
#### @mutation
|
|
594
|
+
|
|
595
|
+
Apply the `@mutation` trait to your Smithy operation to force it to be treated as a mutation:
|
|
596
|
+
|
|
597
|
+
```smithy
|
|
598
|
+
@http(method: "GET", uri: "/start-processing")
|
|
599
|
+
@mutation
|
|
600
|
+
operation StartProcessing {
|
|
601
|
+
input: StartProcessingInput
|
|
602
|
+
output: StartProcessingOutput
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
The generated hook will provide `mutationOptions` even though it uses the `GET` HTTP method:
|
|
607
|
+
|
|
608
|
+
```tsx
|
|
609
|
+
const startProcessing = useMutation(api.startProcessing.mutationOptions());
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Custom Pagination Cursor
|
|
613
|
+
|
|
614
|
+
By default, the generated hooks assume cursor-based pagination with a parameter named `cursor`. You can customize this behavior using the `@cursor` trait which is added to your model project in `extensions.smithy`.
|
|
615
|
+
|
|
616
|
+
Apply the `@cursor` trait with `inputToken` to change the name of the input parameter used for the pagination token:
|
|
617
|
+
|
|
618
|
+
```smithy
|
|
619
|
+
@http(method: "GET", uri: "/items")
|
|
620
|
+
@cursor(inputToken: "nextToken")
|
|
621
|
+
operation ListItems {
|
|
622
|
+
input := {
|
|
623
|
+
nextToken: String
|
|
624
|
+
limit: Integer
|
|
625
|
+
}
|
|
626
|
+
output := {
|
|
627
|
+
items: ItemList
|
|
628
|
+
nextToken: String
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
If you would not like to generate `infiniteQueryOptions` for an operation which has an input parameter named `cursor`, you can disable cursor-based pagination:
|
|
634
|
+
|
|
635
|
+
```smithy
|
|
636
|
+
@cursor(enabled: false)
|
|
637
|
+
operation ListItems {
|
|
638
|
+
input := {
|
|
639
|
+
// Input parameter named 'cursor' will cause this operation to be treated as a paginated operation by default
|
|
640
|
+
cursor: String
|
|
641
|
+
}
|
|
642
|
+
output := {
|
|
643
|
+
...
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Grouping Operations
|
|
649
|
+
|
|
650
|
+
The generated hooks and client methods are automatically organized based on the [`@tags` trait](https://smithy.io/2.0/spec/documentation-traits.html#tags-trait) in your Smithy operations. Operations with the same tags are grouped together, which helps keep your API calls organized and provides better code completion in your IDE.
|
|
651
|
+
|
|
652
|
+
For example, with this Smithy model:
|
|
653
|
+
|
|
654
|
+
```smithy
|
|
655
|
+
service MyService {
|
|
656
|
+
operations: [ListItems, CreateItem, ListUsers, CreateUser]
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
@tags(["items"])
|
|
660
|
+
operation ListItems {
|
|
661
|
+
input: ListItemsInput
|
|
662
|
+
output: ListItemsOutput
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
@tags(["items"])
|
|
666
|
+
operation CreateItem {
|
|
667
|
+
input: CreateItemInput
|
|
668
|
+
output: CreateItemOutput
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
@tags(["users"])
|
|
672
|
+
operation ListUsers {
|
|
673
|
+
input: ListUsersInput
|
|
674
|
+
output: ListUsersOutput
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
@tags(["users"])
|
|
678
|
+
operation CreateUser {
|
|
679
|
+
input: CreateUserInput
|
|
680
|
+
output: CreateUserOutput
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
The generated hooks will be grouped by tags:
|
|
685
|
+
|
|
686
|
+
```tsx
|
|
687
|
+
import { useQuery, useMutation } from '@tanstack/react-query';
|
|
688
|
+
import { useMyApi } from './hooks/useMyApi';
|
|
689
|
+
|
|
690
|
+
function ItemsAndUsers() {
|
|
691
|
+
const api = useMyApi();
|
|
692
|
+
|
|
693
|
+
// Items operations are grouped under api.items
|
|
694
|
+
const items = useQuery(api.items.listItems.queryOptions());
|
|
695
|
+
const createItem = useMutation(api.items.createItem.mutationOptions());
|
|
696
|
+
|
|
697
|
+
// Users operations are grouped under api.users
|
|
698
|
+
const users = useQuery(api.users.listUsers.queryOptions());
|
|
699
|
+
|
|
700
|
+
// Usage example
|
|
701
|
+
const handleCreateItem = () => {
|
|
702
|
+
createItem.mutate({ name: 'New Item' });
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
return (
|
|
706
|
+
<div>
|
|
707
|
+
<h2>Items</h2>
|
|
708
|
+
<ul>
|
|
709
|
+
{items.data?.map(item => (
|
|
710
|
+
<li key={item.id}>{item.name}</li>
|
|
711
|
+
))}
|
|
712
|
+
</ul>
|
|
713
|
+
<button onClick={handleCreateItem}>Add Item</button>
|
|
714
|
+
|
|
715
|
+
<h2>Users</h2>
|
|
716
|
+
<ul>
|
|
717
|
+
{users.data?.map(user => (
|
|
718
|
+
<li key={user.id}>{user.name}</li>
|
|
719
|
+
))}
|
|
720
|
+
</ul>
|
|
721
|
+
</div>
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
This grouping makes it easier to organize your API calls and provides better code completion in your IDE.
|
|
727
|
+
|
|
728
|
+
<Drawer title="Grouped operations using the API client directly" trigger="Click here for an example using the client directly.">
|
|
729
|
+
```tsx
|
|
730
|
+
import { useState, useEffect } from 'react';
|
|
731
|
+
import { useMyApiClient } from './hooks/useMyApiClient';
|
|
732
|
+
|
|
733
|
+
function ItemsAndUsers() {
|
|
734
|
+
const api = useMyApiClient();
|
|
735
|
+
const [items, setItems] = useState([]);
|
|
736
|
+
const [users, setUsers] = useState([]);
|
|
737
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
738
|
+
|
|
739
|
+
// Load data
|
|
740
|
+
useEffect(() => {
|
|
741
|
+
const fetchData = async () => {
|
|
742
|
+
try {
|
|
743
|
+
setIsLoading(true);
|
|
744
|
+
|
|
745
|
+
// Items operations are grouped under api.items
|
|
746
|
+
const itemsData = await api.items.listItems();
|
|
747
|
+
setItems(itemsData);
|
|
748
|
+
|
|
749
|
+
// Users operations are grouped under api.users
|
|
750
|
+
const usersData = await api.users.listUsers();
|
|
751
|
+
setUsers(usersData);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
console.error('Error fetching data:', error);
|
|
754
|
+
} finally {
|
|
755
|
+
setIsLoading(false);
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
fetchData();
|
|
760
|
+
}, [api]);
|
|
761
|
+
|
|
762
|
+
const handleCreateItem = async () => {
|
|
763
|
+
try {
|
|
764
|
+
// Create item using the grouped method
|
|
765
|
+
const newItem = await api.items.createItem({ name: 'New Item' });
|
|
766
|
+
setItems(prevItems => [...prevItems, newItem]);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
console.error('Error creating item:', error);
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
if (isLoading) {
|
|
773
|
+
return <div>Loading...</div>;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return (
|
|
777
|
+
<div>
|
|
778
|
+
<h2>Items</h2>
|
|
779
|
+
<ul>
|
|
780
|
+
{items.map(item => (
|
|
781
|
+
<li key={item.id}>{item.name}</li>
|
|
782
|
+
))}
|
|
783
|
+
</ul>
|
|
784
|
+
<button onClick={handleCreateItem}>Add Item</button>
|
|
785
|
+
|
|
786
|
+
<h2>Users</h2>
|
|
787
|
+
<ul>
|
|
788
|
+
{users.map(user => (
|
|
789
|
+
<li key={user.id}>{user.name}</li>
|
|
790
|
+
))}
|
|
791
|
+
</ul>
|
|
792
|
+
</div>
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
</Drawer>
|
|
797
|
+
|
|
798
|
+
### Errors
|
|
799
|
+
|
|
800
|
+
You can customize error responses in your Smithy API by defining custom error structures in your Smithy model. The generated client will automatically handle these custom error types.
|
|
801
|
+
|
|
802
|
+
#### Defining Custom Error Structures
|
|
803
|
+
|
|
804
|
+
Define your error structures in your Smithy model:
|
|
805
|
+
|
|
806
|
+
```smithy
|
|
807
|
+
@error("client")
|
|
808
|
+
@httpError(400)
|
|
809
|
+
structure InvalidRequestError {
|
|
810
|
+
@required
|
|
811
|
+
message: String
|
|
812
|
+
|
|
813
|
+
fieldErrors: FieldErrorList
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
@error("client")
|
|
817
|
+
@httpError(403)
|
|
818
|
+
structure UnauthorizedError {
|
|
819
|
+
@required
|
|
820
|
+
reason: String
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
@error("server")
|
|
824
|
+
@httpError(500)
|
|
825
|
+
structure InternalServerError {
|
|
826
|
+
@required
|
|
827
|
+
message: String
|
|
828
|
+
|
|
829
|
+
traceId: String
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
list FieldErrorList {
|
|
833
|
+
member: FieldError
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
structure FieldError {
|
|
837
|
+
@required
|
|
838
|
+
field: String
|
|
839
|
+
|
|
840
|
+
@required
|
|
841
|
+
message: String
|
|
842
|
+
}
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
#### Adding Errors to Operations
|
|
846
|
+
|
|
847
|
+
Specify which errors your operations can return:
|
|
848
|
+
|
|
849
|
+
```smithy
|
|
850
|
+
operation CreateItem {
|
|
851
|
+
input: CreateItemInput
|
|
852
|
+
output: CreateItemOutput
|
|
853
|
+
errors: [
|
|
854
|
+
InvalidRequestError
|
|
855
|
+
UnauthorizedError
|
|
856
|
+
InternalServerError
|
|
857
|
+
]
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
operation GetItem {
|
|
861
|
+
input: GetItemInput
|
|
862
|
+
output: GetItemOutput
|
|
863
|
+
errors: [
|
|
864
|
+
ItemNotFoundError
|
|
865
|
+
InternalServerError
|
|
866
|
+
]
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
@error("client")
|
|
870
|
+
@httpError(404)
|
|
871
|
+
structure ItemNotFoundError {
|
|
872
|
+
@required
|
|
873
|
+
message: String
|
|
874
|
+
}
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
#### Using Custom Error Types in React
|
|
878
|
+
|
|
879
|
+
The generated client will automatically handle these custom error types, allowing you to type-check and handle different error responses:
|
|
880
|
+
|
|
881
|
+
```tsx
|
|
882
|
+
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
883
|
+
|
|
884
|
+
function ItemComponent() {
|
|
885
|
+
const api = useMyApi();
|
|
886
|
+
|
|
887
|
+
// Query with typed error handling
|
|
888
|
+
const getItem = useQuery({
|
|
889
|
+
...api.getItem.queryOptions({ itemId: '123' }),
|
|
890
|
+
onError: (error) => {
|
|
891
|
+
// Error is typed based on the errors in your Smithy model
|
|
892
|
+
switch (error.status) {
|
|
893
|
+
case 404:
|
|
894
|
+
// error.error is typed as ItemNotFoundError
|
|
895
|
+
console.error('Not found:', error.error.message);
|
|
896
|
+
break;
|
|
897
|
+
case 500:
|
|
898
|
+
// error.error is typed as InternalServerError
|
|
899
|
+
console.error('Server error:', error.error.message);
|
|
900
|
+
console.error('Trace ID:', error.error.traceId);
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
// Mutation with typed error handling
|
|
907
|
+
const createItem = useMutation({
|
|
908
|
+
...api.createItem.mutationOptions(),
|
|
909
|
+
onError: (error) => {
|
|
910
|
+
switch (error.status) {
|
|
911
|
+
case 400:
|
|
912
|
+
// error.error is typed as InvalidRequestError
|
|
913
|
+
console.error('Validation error:', error.error.message);
|
|
914
|
+
console.error('Field errors:', error.error.fieldErrors);
|
|
915
|
+
break;
|
|
916
|
+
case 403:
|
|
917
|
+
// error.error is typed as UnauthorizedError
|
|
918
|
+
console.error('Unauthorized:', error.error.reason);
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// Component rendering with error handling
|
|
925
|
+
if (getItem.isError) {
|
|
926
|
+
if (getItem.error.status === 404) {
|
|
927
|
+
return <NotFoundMessage message={getItem.error.error.message} />;
|
|
928
|
+
} else if (getItem.error.status === 500) {
|
|
929
|
+
return <ErrorMessage message={getItem.error.error.message} />;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return (
|
|
934
|
+
<div>
|
|
935
|
+
{/* Component content */}
|
|
936
|
+
</div>
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
<Drawer title="Handling custom errors with the client directly" trigger="Click here for an example using the client directly.">
|
|
942
|
+
```tsx
|
|
943
|
+
import { useState, useEffect } from 'react';
|
|
944
|
+
|
|
945
|
+
function ItemComponent() {
|
|
946
|
+
const api = useMyApiClient();
|
|
947
|
+
const [item, setItem] = useState(null);
|
|
948
|
+
const [error, setError] = useState(null);
|
|
949
|
+
const [loading, setLoading] = useState(true);
|
|
950
|
+
|
|
951
|
+
// Fetch item with error handling
|
|
952
|
+
useEffect(() => {
|
|
953
|
+
const fetchItem = async () => {
|
|
954
|
+
try {
|
|
955
|
+
setLoading(true);
|
|
956
|
+
const data = await api.getItem({ itemId: '123' });
|
|
957
|
+
setItem(data);
|
|
958
|
+
} catch (e) {
|
|
959
|
+
// Error is typed based on the errors in your Smithy model
|
|
960
|
+
const err = e as GetItemError;
|
|
961
|
+
setError(err);
|
|
962
|
+
|
|
963
|
+
switch (err.status) {
|
|
964
|
+
case 404:
|
|
965
|
+
// err.error is typed as ItemNotFoundError
|
|
966
|
+
console.error('Not found:', err.error.message);
|
|
967
|
+
break;
|
|
968
|
+
case 500:
|
|
969
|
+
// err.error is typed as InternalServerError
|
|
970
|
+
console.error('Server error:', err.error.message);
|
|
971
|
+
console.error('Trace ID:', err.error.traceId);
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
} finally {
|
|
975
|
+
setLoading(false);
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
fetchItem();
|
|
980
|
+
}, [api]);
|
|
981
|
+
|
|
982
|
+
// Create item with error handling
|
|
983
|
+
const handleCreateItem = async (data) => {
|
|
984
|
+
try {
|
|
985
|
+
await api.createItem(data);
|
|
986
|
+
} catch (e) {
|
|
987
|
+
const err = e as CreateItemError;
|
|
988
|
+
|
|
989
|
+
switch (err.status) {
|
|
990
|
+
case 400:
|
|
991
|
+
// err.error is typed as InvalidRequestError
|
|
992
|
+
console.error('Validation error:', err.error.message);
|
|
993
|
+
console.error('Field errors:', err.error.fieldErrors);
|
|
994
|
+
break;
|
|
995
|
+
case 403:
|
|
996
|
+
// err.error is typed as UnauthorizedError
|
|
997
|
+
console.error('Unauthorized:', err.error.reason);
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
// Component rendering with error handling
|
|
1004
|
+
if (loading) {
|
|
1005
|
+
return <LoadingSpinner />;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (error) {
|
|
1009
|
+
if (error.status === 404) {
|
|
1010
|
+
return <NotFoundMessage message={error.error.message} />;
|
|
1011
|
+
} else if (error.status === 500) {
|
|
1012
|
+
return <ErrorMessage message={error.error.message} />;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return (
|
|
1017
|
+
<div>
|
|
1018
|
+
{/* Component content */}
|
|
1019
|
+
</div>
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
```
|
|
1023
|
+
</Drawer>
|
|
1024
|
+
|
|
1025
|
+
:::tip[Error Structures]
|
|
1026
|
+
When defining error structures in Smithy, always use the `@error` and `@httpError` traits to specify the error type and HTTP status code. This ensures that the generated client will have proper type information for error handling.
|
|
1027
|
+
:::
|
|
1028
|
+
|
|
1029
|
+
## Best Practices
|
|
1030
|
+
|
|
1031
|
+
### Handle Loading States
|
|
1032
|
+
|
|
1033
|
+
Always handle loading and error states for a better user experience:
|
|
1034
|
+
|
|
1035
|
+
```tsx
|
|
1036
|
+
import { useQuery } from '@tanstack/react-query';
|
|
1037
|
+
|
|
1038
|
+
function ItemList() {
|
|
1039
|
+
const api = useMyApi();
|
|
1040
|
+
const items = useQuery(api.listItems.queryOptions());
|
|
1041
|
+
|
|
1042
|
+
if (items.isLoading) {
|
|
1043
|
+
return <LoadingSpinner />;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (items.isError) {
|
|
1047
|
+
const err = items.error;
|
|
1048
|
+
switch (err.status) {
|
|
1049
|
+
case 403:
|
|
1050
|
+
// err.error is typed as ListItems403Response
|
|
1051
|
+
return <ErrorMessage message={err.error.reason} />;
|
|
1052
|
+
case 500:
|
|
1053
|
+
case 502:
|
|
1054
|
+
// err.error is typed as ListItems5XXResponse
|
|
1055
|
+
return (
|
|
1056
|
+
<ErrorMessage
|
|
1057
|
+
message={err.error.message}
|
|
1058
|
+
/>
|
|
1059
|
+
);
|
|
1060
|
+
default:
|
|
1061
|
+
return <ErrorMessage message="An unknown error occurred" />;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return (
|
|
1066
|
+
<ul>
|
|
1067
|
+
{items.data.map((item) => (
|
|
1068
|
+
<li key={item.id}>{item.name}</li>
|
|
1069
|
+
))}
|
|
1070
|
+
</ul>
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
<Drawer title="Handle loading states using the API client directly" trigger="Click here for an example using the vanilla client directly.">
|
|
1076
|
+
```tsx
|
|
1077
|
+
function ItemList() {
|
|
1078
|
+
const api = useMyApiClient();
|
|
1079
|
+
const [items, setItems] = useState([]);
|
|
1080
|
+
const [loading, setLoading] = useState(true);
|
|
1081
|
+
const [error, setError] = useState(null);
|
|
1082
|
+
|
|
1083
|
+
useEffect(() => {
|
|
1084
|
+
const fetchItems = async () => {
|
|
1085
|
+
try {
|
|
1086
|
+
const data = await api.listItems();
|
|
1087
|
+
setItems(data);
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
setError(err);
|
|
1090
|
+
} finally {
|
|
1091
|
+
setLoading(false);
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
fetchItems();
|
|
1095
|
+
}, [api]);
|
|
1096
|
+
|
|
1097
|
+
if (loading) {
|
|
1098
|
+
return <LoadingSpinner />;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (error) {
|
|
1102
|
+
const err = error as ListItemsError;
|
|
1103
|
+
switch (err.status) {
|
|
1104
|
+
case 403:
|
|
1105
|
+
// err.error is typed as ListItems403Response
|
|
1106
|
+
return <ErrorMessage message={err.error.reason} />;
|
|
1107
|
+
case 500:
|
|
1108
|
+
case 502:
|
|
1109
|
+
// err.error is typed as ListItems5XXResponse
|
|
1110
|
+
return (
|
|
1111
|
+
<ErrorMessage
|
|
1112
|
+
message={err.error.message}
|
|
1113
|
+
/>
|
|
1114
|
+
);
|
|
1115
|
+
default:
|
|
1116
|
+
return <ErrorMessage message="An unknown error occurred" />;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
return (
|
|
1121
|
+
<ul>
|
|
1122
|
+
{items.map((item) => (
|
|
1123
|
+
<li key={item.id}>{item.name}</li>
|
|
1124
|
+
))}
|
|
1125
|
+
</ul>
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
```
|
|
1129
|
+
</Drawer>
|
|
1130
|
+
|
|
1131
|
+
### Optimistic Updates
|
|
1132
|
+
|
|
1133
|
+
Implement optimistic updates for a better user experience:
|
|
1134
|
+
|
|
1135
|
+
```tsx
|
|
1136
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
1137
|
+
|
|
1138
|
+
function ItemList() {
|
|
1139
|
+
const api = useMyApi();
|
|
1140
|
+
const queryClient = useQueryClient();
|
|
1141
|
+
|
|
1142
|
+
// Query to fetch items
|
|
1143
|
+
const itemsQuery = useQuery(api.listItems.queryOptions());
|
|
1144
|
+
|
|
1145
|
+
// Mutation for deleting items with optimistic updates
|
|
1146
|
+
const deleteMutation = useMutation({
|
|
1147
|
+
...api.deleteItem.mutationOptions(),
|
|
1148
|
+
onMutate: async (itemId) => {
|
|
1149
|
+
// Cancel any outgoing refetches
|
|
1150
|
+
await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
|
|
1151
|
+
|
|
1152
|
+
// Snapshot the previous value
|
|
1153
|
+
const previousItems = queryClient.getQueryData(api.listItems.queryKey());
|
|
1154
|
+
|
|
1155
|
+
// Optimistically update to the new value
|
|
1156
|
+
queryClient.setQueryData(
|
|
1157
|
+
api.listItems.queryKey(),
|
|
1158
|
+
(old) => old.filter((item) => item.id !== itemId)
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
// Return a context object with the snapshot
|
|
1162
|
+
return { previousItems };
|
|
1163
|
+
},
|
|
1164
|
+
onError: (err, itemId, context) => {
|
|
1165
|
+
// If the mutation fails, use the context returned from onMutate to roll back
|
|
1166
|
+
queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
|
|
1167
|
+
console.error('Failed to delete item:', err);
|
|
1168
|
+
},
|
|
1169
|
+
onSettled: () => {
|
|
1170
|
+
// Always refetch after error or success to ensure data is in sync with server
|
|
1171
|
+
queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
if (itemsQuery.isLoading) {
|
|
1176
|
+
return <LoadingSpinner />;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (itemsQuery.isError) {
|
|
1180
|
+
return <ErrorMessage message="Failed to load items" />;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
return (
|
|
1184
|
+
<ul>
|
|
1185
|
+
{itemsQuery.data.map((item) => (
|
|
1186
|
+
<li key={item.id}>
|
|
1187
|
+
{item.name}
|
|
1188
|
+
<button
|
|
1189
|
+
onClick={() => deleteMutation.mutate(item.id)}
|
|
1190
|
+
disabled={deleteMutation.isPending}
|
|
1191
|
+
>
|
|
1192
|
+
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
|
|
1193
|
+
</button>
|
|
1194
|
+
</li>
|
|
1195
|
+
))}
|
|
1196
|
+
</ul>
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
```
|
|
1200
|
+
|
|
1201
|
+
<Drawer title="Optimistic updates using the API client directly" trigger="Click here for an example using the vanilla client directly.">
|
|
1202
|
+
```tsx
|
|
1203
|
+
function ItemList() {
|
|
1204
|
+
const api = useMyApiClient();
|
|
1205
|
+
const [items, setItems] = useState([]);
|
|
1206
|
+
|
|
1207
|
+
const handleDelete = async (itemId) => {
|
|
1208
|
+
// Optimistically remove the item
|
|
1209
|
+
const previousItems = items;
|
|
1210
|
+
setItems(items.filter((item) => item.id !== itemId));
|
|
1211
|
+
|
|
1212
|
+
try {
|
|
1213
|
+
await api.deleteItem(itemId);
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
// Restore previous items on error
|
|
1216
|
+
setItems(previousItems);
|
|
1217
|
+
console.error('Failed to delete item:', error);
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
return (
|
|
1222
|
+
<ul>
|
|
1223
|
+
{items.map((item) => (
|
|
1224
|
+
<li key={item.id}>
|
|
1225
|
+
{item.name}
|
|
1226
|
+
<button onClick={() => handleDelete(item.id)}>Delete</button>
|
|
1227
|
+
</li>
|
|
1228
|
+
))}
|
|
1229
|
+
</ul>
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
```
|
|
1233
|
+
</Drawer>
|
|
1234
|
+
|
|
1235
|
+
## Type Safety
|
|
1236
|
+
|
|
1237
|
+
The integration provides complete end-to-end type safety. Your IDE will provide full autocompletion and type checking for all your API calls:
|
|
1238
|
+
|
|
1239
|
+
```tsx
|
|
1240
|
+
import { useMutation } from '@tanstack/react-query';
|
|
1241
|
+
|
|
1242
|
+
function ItemForm() {
|
|
1243
|
+
const api = useMyApi();
|
|
1244
|
+
|
|
1245
|
+
// Type-safe mutation for creating items
|
|
1246
|
+
const createItem = useMutation({
|
|
1247
|
+
...api.createItem.mutationOptions(),
|
|
1248
|
+
// ✅ Type error if onSuccess callback doesn't handle the correct response type
|
|
1249
|
+
onSuccess: (data) => {
|
|
1250
|
+
// data is fully typed based on your API's response schema
|
|
1251
|
+
console.log(`Item created with ID: ${data.id}`);
|
|
1252
|
+
},
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
const handleSubmit = (data: CreateItemInput) => {
|
|
1256
|
+
// ✅ Type error if input doesn't match schema
|
|
1257
|
+
createItem.mutate(data);
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
// Error UI can use type narrowing to handle different error types
|
|
1261
|
+
if (createItem.error) {
|
|
1262
|
+
const error = createItem.error;
|
|
1263
|
+
switch (error.status) {
|
|
1264
|
+
case 400:
|
|
1265
|
+
// error.error is typed as InvalidRequestError
|
|
1266
|
+
return (
|
|
1267
|
+
<FormError
|
|
1268
|
+
message="Invalid input"
|
|
1269
|
+
errors={error.error.fieldErrors}
|
|
1270
|
+
/>
|
|
1271
|
+
);
|
|
1272
|
+
case 403:
|
|
1273
|
+
// error.error is typed as UnauthorizedError
|
|
1274
|
+
return <AuthError reason={error.error.reason} />;
|
|
1275
|
+
default:
|
|
1276
|
+
// error.error is typed as InternalServerError for 500, etc.
|
|
1277
|
+
return <ServerError message={error.error.message} />;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return (
|
|
1282
|
+
<form onSubmit={(e) => {
|
|
1283
|
+
e.preventDefault();
|
|
1284
|
+
handleSubmit({ name: 'New Item' });
|
|
1285
|
+
}}>
|
|
1286
|
+
{/* Form fields */}
|
|
1287
|
+
<button
|
|
1288
|
+
type="submit"
|
|
1289
|
+
disabled={createItem.isPending}
|
|
1290
|
+
>
|
|
1291
|
+
{createItem.isPending ? 'Creating...' : 'Create Item'}
|
|
1292
|
+
</button>
|
|
1293
|
+
</form>
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
<Drawer title="Type safety using the API client directly" trigger="Click here for an example using the vanilla client directly.">
|
|
1299
|
+
```tsx
|
|
1300
|
+
function ItemForm() {
|
|
1301
|
+
const api = useMyApiClient();
|
|
1302
|
+
const [error, setError] = useState<CreateItemError | null>(null);
|
|
1303
|
+
|
|
1304
|
+
const handleSubmit = async (data: CreateItemInput) => {
|
|
1305
|
+
try {
|
|
1306
|
+
// ✅ Type error if input doesn't match schema
|
|
1307
|
+
await api.createItem(data);
|
|
1308
|
+
} catch (e) {
|
|
1309
|
+
// ✅ Error type includes all possible error responses
|
|
1310
|
+
const err = e as CreateItemError;
|
|
1311
|
+
switch (err.status) {
|
|
1312
|
+
case 400:
|
|
1313
|
+
// err.error is typed as InvalidRequestError
|
|
1314
|
+
console.error('Validation errors:', err.error.fieldErrors);
|
|
1315
|
+
break;
|
|
1316
|
+
case 403:
|
|
1317
|
+
// err.error is typed as UnauthorizedError
|
|
1318
|
+
console.error('Not authorized:', err.error.reason);
|
|
1319
|
+
break;
|
|
1320
|
+
case 500:
|
|
1321
|
+
// err.error is typed as InternalServerError
|
|
1322
|
+
console.error('Server error:', err.error.message);
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
setError(err);
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
// Error UI can use type narrowing to handle different error types
|
|
1330
|
+
if (error) {
|
|
1331
|
+
switch (error.status) {
|
|
1332
|
+
case 400:
|
|
1333
|
+
return (
|
|
1334
|
+
<FormError
|
|
1335
|
+
message="Invalid input"
|
|
1336
|
+
errors={error.error.fieldErrors}
|
|
1337
|
+
/>
|
|
1338
|
+
);
|
|
1339
|
+
case 403:
|
|
1340
|
+
return <AuthError reason={error.error.reason} />;
|
|
1341
|
+
default:
|
|
1342
|
+
return <ServerError message={error.error.message} />;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
return <form onSubmit={handleSubmit}>{/* ... */}</form>;
|
|
1347
|
+
}
|
|
1348
|
+
```
|
|
1349
|
+
</Drawer>
|
|
1350
|
+
|
|
1351
|
+
The types are automatically generated from your Smithy API's OpenAPI schema, ensuring that any changes to your API are reflected in your frontend code after a build.
|
|
1352
|
+
|
|
1353
|
+
## Custom Auth
|
|
1354
|
+
|
|
1355
|
+
If your Smithy API 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.
|