@aws/nx-plugin-mcp 1.0.0-rc.96 → 1.0.0-rc.98
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 +90 -45
- package/docs/get_started/existing-project.mdx +7 -4
- package/docs/get_started/quick-start.mdx +58 -5
- package/docs/get_started/tutorials/dungeon-game/1.mdx +23 -20
- package/docs/get_started/tutorials/dungeon-game/2.mdx +11 -3
- package/docs/get_started/tutorials/dungeon-game/3.mdx +4 -0
- package/docs/get_started/tutorials/dungeon-game/4.mdx +7 -2
- package/docs/get_started/tutorials/dungeon-game/wrap-up.mdx +10 -1
- package/docs/guides/agentcore-gateway.mdx +4 -2
- package/docs/guides/agentcore-harness.mdx +2 -1
- package/docs/guides/astro-docs.mdx +25 -7
- package/docs/guides/connection/py-agent-a2a.mdx +2 -0
- package/docs/guides/connection/py-agent-dynamodb.mdx +1 -1
- package/docs/guides/connection/py-agent-gateway.mdx +3 -0
- package/docs/guides/connection/py-agent-mcp.mdx +18 -4
- package/docs/guides/connection/py-agent-rdb.mdx +5 -4
- package/docs/guides/connection/py-fast-api-dynamodb.mdx +1 -1
- package/docs/guides/connection/py-fast-api-rdb.mdx +7 -2
- package/docs/guides/connection/py-mcp-server-dynamodb.mdx +1 -1
- package/docs/guides/connection/py-mcp-server-rdb.mdx +6 -6
- package/docs/guides/connection/react-agui.mdx +34 -25
- package/docs/guides/connection/react-fastapi.mdx +114 -116
- package/docs/guides/connection/react-py-agent.mdx +4 -0
- package/docs/guides/connection/react-smithy.mdx +152 -98
- package/docs/guides/connection/react-trpc.mdx +13 -6
- package/docs/guides/connection/smithy-dynamodb.mdx +2 -8
- package/docs/guides/connection/smithy-rdb.mdx +3 -6
- package/docs/guides/connection/trpc-rdb.mdx +6 -6
- package/docs/guides/connection/ts-agent-a2a.mdx +4 -4
- package/docs/guides/connection/ts-agent-dynamodb.mdx +13 -11
- package/docs/guides/connection/ts-agent-gateway.mdx +2 -0
- package/docs/guides/connection/ts-agent-mcp.mdx +19 -7
- package/docs/guides/connection/ts-agent-rdb.mdx +2 -2
- package/docs/guides/connection/ts-mcp-server-dynamodb.mdx +11 -7
- package/docs/guides/connection/ts-mcp-server-rdb.mdx +4 -3
- package/docs/guides/docker-bundling.mdx +23 -3
- package/docs/guides/fastapi.mdx +16 -5
- package/docs/guides/license.mdx +30 -6
- package/docs/guides/nx-generator.mdx +11 -12
- package/docs/guides/py-agent.mdx +129 -54
- package/docs/guides/py-mcp-server.mdx +3 -1
- package/docs/guides/py-rdb.mdx +13 -4
- package/docs/guides/python-lambda-function.mdx +8 -8
- package/docs/guides/python-project.mdx +28 -25
- package/docs/guides/react-website-auth.mdx +8 -8
- package/docs/guides/react-website.mdx +46 -27
- package/docs/guides/runtime-config.mdx +24 -4
- package/docs/guides/security.mdx +1 -1
- package/docs/guides/terraform-project.mdx +8 -2
- package/docs/guides/trpc.mdx +96 -12
- package/docs/guides/ts-agent.mdx +17 -3
- package/docs/guides/ts-dcr-proxy.mdx +24 -6
- package/docs/guides/ts-lambda-function.mdx +7 -1
- package/docs/guides/ts-mcp-server.mdx +45 -15
- package/docs/guides/ts-nx-plugin.mdx +17 -7
- package/docs/guides/ts-rdb.mdx +9 -2
- package/docs/guides/ts-smithy-api.mdx +76 -7
- package/docs/guides/typescript-infrastructure.mdx +27 -11
- package/docs/guides/typescript-project.mdx +12 -5
- package/docs/guides/workspace.mdx +21 -9
- package/docs/snippets/api/type-safe-api-integrations.mdx +2 -0
- package/docs/snippets/connection/a2a-infrastructure.mdx +3 -0
- package/docs/snippets/connection/infra-project-prerequisite.mdx +8 -0
- package/docs/snippets/connection/strands-agent-rdb-ssl-requirements.mdx +1 -1
- package/docs/snippets/connection/ts-lambda-rdb-ssl-requirements.mdx +1 -1
- package/docs/snippets/connection/ts-mcp-server-rdb-ssl-requirements.mdx +1 -1
- package/docs/snippets/pdk-migration/faq/type-safe-api.mdx +1 -1
- package/docs/snippets/prerequisites.mdx +1 -1
- package/docs/snippets/required-prerequisites.mdx +1 -1
- package/package.json +1 -1
- package/src/init/schema.json +5 -0
- package/src/py/project/schema.json +3 -1
|
@@ -126,6 +126,10 @@ For more fine-grained control, you can use the `watch-generate:<ApiName>-client`
|
|
|
126
126
|
/>
|
|
127
127
|
:::
|
|
128
128
|
|
|
129
|
+
:::note[The dev target requires the Nx Daemon]
|
|
130
|
+
The `dev` target hot-reloads local dev servers. Many of these rely on [`nx watch`](https://nx.dev/docs/guides/tasks--caching/workspace-watching) to achieve this, which requires the [Nx Daemon](https://nx.dev/docs/concepts/nx-daemon) to be enabled.
|
|
131
|
+
:::
|
|
132
|
+
|
|
129
133
|
### Using the API Hook
|
|
130
134
|
|
|
131
135
|
The generator provides a `use<ApiName>` hook which you can use to call your API with TanStack Query.
|
|
@@ -143,13 +147,15 @@ function MyComponent() {
|
|
|
143
147
|
const api = useMyApi();
|
|
144
148
|
const item = useQuery(api.getItem.queryOptions({ itemId: 'some-id' }));
|
|
145
149
|
|
|
146
|
-
if (item.
|
|
147
|
-
if (item.isError) return <div>Error: {item.error.
|
|
150
|
+
if (item.isPending) return <div>Loading...</div>;
|
|
151
|
+
if (item.isError) return <div>Error: {JSON.stringify(item.error.error)}</div>;
|
|
148
152
|
|
|
149
153
|
return <div>Item: {item.data.name}</div>;
|
|
150
154
|
}
|
|
151
155
|
```
|
|
152
156
|
|
|
157
|
+
The error is a discriminated union of `{ status, error }` objects rather than an `Error`, so there is no top-level `message` to render. Narrow on `status` to read a specific response body — see [Error Handling](#error-handling) below.
|
|
158
|
+
|
|
153
159
|
<Drawer title="Using the API client directly" trigger="Click here for an example using the vanilla client directly.">
|
|
154
160
|
```tsx {5,13}
|
|
155
161
|
import { useState, useEffect } from 'react';
|
|
@@ -219,7 +225,7 @@ function CreateItemForm() {
|
|
|
219
225
|
|
|
220
226
|
{createItem.isError && (
|
|
221
227
|
<div className="error">
|
|
222
|
-
Error: {createItem.error.
|
|
228
|
+
Error: {JSON.stringify(createItem.error.error)}
|
|
223
229
|
</div>
|
|
224
230
|
)}
|
|
225
231
|
</form>
|
|
@@ -245,7 +251,7 @@ const createItem = useMutation({
|
|
|
245
251
|
onSettled: () => {
|
|
246
252
|
// This will run when the mutation completes (success or error)
|
|
247
253
|
// Good place to invalidate queries that might be affected
|
|
248
|
-
queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
|
|
254
|
+
queryClient.invalidateQueries({ queryKey: api.listItems.queryKey({}) });
|
|
249
255
|
}
|
|
250
256
|
});
|
|
251
257
|
```
|
|
@@ -331,12 +337,12 @@ function ItemList() {
|
|
|
331
337
|
}),
|
|
332
338
|
});
|
|
333
339
|
|
|
334
|
-
if (items.
|
|
340
|
+
if (items.isPending) {
|
|
335
341
|
return <LoadingSpinner />;
|
|
336
342
|
}
|
|
337
343
|
|
|
338
344
|
if (items.isError) {
|
|
339
|
-
return <ErrorMessage message={items.error.
|
|
345
|
+
return <ErrorMessage message={JSON.stringify(items.error.error)} />;
|
|
340
346
|
}
|
|
341
347
|
|
|
342
348
|
return (
|
|
@@ -456,7 +462,9 @@ function ItemList() {
|
|
|
456
462
|
|
|
457
463
|
### Error Handling
|
|
458
464
|
|
|
459
|
-
The integration includes built-in error handling with typed error responses. An `<
|
|
465
|
+
The integration includes built-in error handling with typed error responses. An `<OperationName>Error` type is generated which is a union of one `<OperationName><StatusCode>Error` member per error status the operation models. Each member has a `status` and an `error` property, so switching on `status` narrows `error` to that status's payload.
|
|
466
|
+
|
|
467
|
+
The example below assumes `CreateItem` models the [error structures defined further down this page](#errors) — a `422` `InvalidRequestError`, a `403` `UnauthorizedError` and a `500` `InternalServerError`. Only the statuses your model declares appear in the union, so a `case` for a status you haven't modelled is a compile error.
|
|
460
468
|
|
|
461
469
|
```tsx {12}
|
|
462
470
|
import { useMutation } from '@tanstack/react-query';
|
|
@@ -471,8 +479,8 @@ function MyComponent() {
|
|
|
471
479
|
|
|
472
480
|
if (createItem.error) {
|
|
473
481
|
switch (createItem.error.status) {
|
|
474
|
-
case
|
|
475
|
-
// error.error is typed as
|
|
482
|
+
case 422:
|
|
483
|
+
// error.error is typed as InvalidRequestErrorResponseContent
|
|
476
484
|
return (
|
|
477
485
|
<div>
|
|
478
486
|
<h2>Invalid input:</h2>
|
|
@@ -480,7 +488,7 @@ function MyComponent() {
|
|
|
480
488
|
</div>
|
|
481
489
|
);
|
|
482
490
|
case 403:
|
|
483
|
-
// error.error is typed as
|
|
491
|
+
// error.error is typed as UnauthorizedErrorResponseContent
|
|
484
492
|
return (
|
|
485
493
|
<div>
|
|
486
494
|
<h2>Not authorized:</h2>
|
|
@@ -488,8 +496,7 @@ function MyComponent() {
|
|
|
488
496
|
</div>
|
|
489
497
|
);
|
|
490
498
|
case 500:
|
|
491
|
-
|
|
492
|
-
// error.error is typed as CreateItem5XXResponse
|
|
499
|
+
// error.error is typed as InternalServerErrorResponseContent
|
|
493
500
|
return (
|
|
494
501
|
<div>
|
|
495
502
|
<h2>Server error:</h2>
|
|
@@ -520,8 +527,8 @@ function MyComponent() {
|
|
|
520
527
|
|
|
521
528
|
if (error) {
|
|
522
529
|
switch (error.status) {
|
|
523
|
-
case
|
|
524
|
-
// error.error is typed as
|
|
530
|
+
case 422:
|
|
531
|
+
// error.error is typed as InvalidRequestErrorResponseContent
|
|
525
532
|
return (
|
|
526
533
|
<div>
|
|
527
534
|
<h2>Invalid input:</h2>
|
|
@@ -529,7 +536,7 @@ function MyComponent() {
|
|
|
529
536
|
</div>
|
|
530
537
|
);
|
|
531
538
|
case 403:
|
|
532
|
-
// error.error is typed as
|
|
539
|
+
// error.error is typed as UnauthorizedErrorResponseContent
|
|
533
540
|
return (
|
|
534
541
|
<div>
|
|
535
542
|
<h2>Not authorized:</h2>
|
|
@@ -537,8 +544,7 @@ function MyComponent() {
|
|
|
537
544
|
</div>
|
|
538
545
|
);
|
|
539
546
|
case 500:
|
|
540
|
-
|
|
541
|
-
// error.error is typed as CreateItem5XXResponse
|
|
547
|
+
// error.error is typed as InternalServerErrorResponseContent
|
|
542
548
|
return (
|
|
543
549
|
<div>
|
|
544
550
|
<h2>Server error:</h2>
|
|
@@ -573,21 +579,21 @@ These map to OpenAPI vendor extensions using the [`@specificationExtension` trai
|
|
|
573
579
|
|
|
574
580
|
#### @query
|
|
575
581
|
|
|
576
|
-
|
|
582
|
+
Apply the `@query` trait to your Smithy operation to force it to be treated as a query:
|
|
577
583
|
|
|
578
584
|
```smithy
|
|
579
|
-
@http(method: "POST", uri: "/items")
|
|
585
|
+
@http(method: "POST", uri: "/search-items")
|
|
580
586
|
@query
|
|
581
|
-
operation
|
|
582
|
-
input:
|
|
583
|
-
output:
|
|
587
|
+
operation SearchItems {
|
|
588
|
+
input: SearchItemsInput
|
|
589
|
+
output: SearchItemsOutput
|
|
584
590
|
}
|
|
585
591
|
```
|
|
586
592
|
|
|
587
593
|
The generated hook will provide `queryOptions` even though it uses the `POST` HTTP method:
|
|
588
594
|
|
|
589
595
|
```tsx
|
|
590
|
-
const items = useQuery(api.
|
|
596
|
+
const items = useQuery(api.searchItems.queryOptions({ term: 'widget' }));
|
|
591
597
|
```
|
|
592
598
|
|
|
593
599
|
#### @mutation
|
|
@@ -607,6 +613,8 @@ The generated hook will provide `mutationOptions` even though it uses the `GET`
|
|
|
607
613
|
|
|
608
614
|
```tsx
|
|
609
615
|
const startProcessing = useMutation(api.startProcessing.mutationOptions());
|
|
616
|
+
|
|
617
|
+
startProcessing.mutate({ id: 'some-id' });
|
|
610
618
|
```
|
|
611
619
|
|
|
612
620
|
### Custom Pagination Cursor
|
|
@@ -616,39 +624,60 @@ By default, the generated hooks assume cursor-based pagination with a parameter
|
|
|
616
624
|
Apply the `@cursor` trait with `inputToken` to change the name of the input parameter used for the pagination token:
|
|
617
625
|
|
|
618
626
|
```smithy
|
|
619
|
-
@http(method: "GET", uri: "/items")
|
|
627
|
+
@http(method: "GET", uri: "/paged-items")
|
|
620
628
|
@cursor(inputToken: "nextToken")
|
|
621
|
-
operation
|
|
629
|
+
operation ListPagedItems {
|
|
622
630
|
input := {
|
|
631
|
+
@httpQuery("nextToken")
|
|
623
632
|
nextToken: String
|
|
633
|
+
@httpQuery("limit")
|
|
624
634
|
limit: Integer
|
|
625
635
|
}
|
|
626
636
|
output := {
|
|
637
|
+
@required
|
|
627
638
|
items: ItemList
|
|
628
639
|
nextToken: String
|
|
629
640
|
}
|
|
630
641
|
}
|
|
631
642
|
```
|
|
632
643
|
|
|
644
|
+
`infiniteQueryOptions` then pages on `nextToken`:
|
|
645
|
+
|
|
646
|
+
```tsx
|
|
647
|
+
const items = useInfiniteQuery({
|
|
648
|
+
...api.listPagedItems.infiniteQueryOptions(
|
|
649
|
+
{ limit: 10 },
|
|
650
|
+
{ getNextPageParam: (lastPage) => lastPage.nextToken || undefined },
|
|
651
|
+
),
|
|
652
|
+
});
|
|
653
|
+
```
|
|
654
|
+
|
|
633
655
|
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
656
|
|
|
635
657
|
```smithy
|
|
658
|
+
@http(method: "GET", uri: "/unpaged-items")
|
|
636
659
|
@cursor(enabled: false)
|
|
637
|
-
operation
|
|
660
|
+
operation ListUnpagedItems {
|
|
638
661
|
input := {
|
|
639
662
|
// Input parameter named 'cursor' will cause this operation to be treated as a paginated operation by default
|
|
663
|
+
@httpQuery("cursor")
|
|
640
664
|
cursor: String
|
|
641
665
|
}
|
|
642
666
|
output := {
|
|
643
|
-
|
|
667
|
+
@required
|
|
668
|
+
items: ItemList
|
|
644
669
|
}
|
|
645
670
|
}
|
|
646
671
|
```
|
|
647
672
|
|
|
673
|
+
`api.listUnpagedItems` then only provides `queryOptions`, `queryKey` and `queryFilter`.
|
|
674
|
+
|
|
648
675
|
### Grouping Operations
|
|
649
676
|
|
|
650
677
|
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
678
|
|
|
679
|
+
Within a group each operation keeps its own camelCased name, so an operation named `ListItems` tagged `"items"` is reached at `api.items.listItems` — not `api.items.list`.
|
|
680
|
+
|
|
652
681
|
For example, with this Smithy model:
|
|
653
682
|
|
|
654
683
|
```smithy
|
|
@@ -657,27 +686,51 @@ service MyService {
|
|
|
657
686
|
}
|
|
658
687
|
|
|
659
688
|
@tags(["items"])
|
|
689
|
+
@http(method: "GET", uri: "/items")
|
|
690
|
+
@readonly
|
|
660
691
|
operation ListItems {
|
|
661
|
-
input
|
|
662
|
-
output
|
|
692
|
+
input := {}
|
|
693
|
+
output := {
|
|
694
|
+
@required
|
|
695
|
+
items: ItemList
|
|
696
|
+
}
|
|
663
697
|
}
|
|
664
698
|
|
|
665
699
|
@tags(["items"])
|
|
700
|
+
@http(method: "POST", uri: "/items")
|
|
666
701
|
operation CreateItem {
|
|
667
|
-
input
|
|
668
|
-
|
|
702
|
+
input := {
|
|
703
|
+
@required
|
|
704
|
+
name: String
|
|
705
|
+
}
|
|
706
|
+
output := {
|
|
707
|
+
@required
|
|
708
|
+
id: String
|
|
709
|
+
}
|
|
669
710
|
}
|
|
670
711
|
|
|
671
712
|
@tags(["users"])
|
|
713
|
+
@http(method: "GET", uri: "/users")
|
|
714
|
+
@readonly
|
|
672
715
|
operation ListUsers {
|
|
673
|
-
input
|
|
674
|
-
output
|
|
716
|
+
input := {}
|
|
717
|
+
output := {
|
|
718
|
+
@required
|
|
719
|
+
users: UserList
|
|
720
|
+
}
|
|
675
721
|
}
|
|
676
722
|
|
|
677
723
|
@tags(["users"])
|
|
724
|
+
@http(method: "POST", uri: "/users")
|
|
678
725
|
operation CreateUser {
|
|
679
|
-
input
|
|
680
|
-
|
|
726
|
+
input := {
|
|
727
|
+
@required
|
|
728
|
+
name: String
|
|
729
|
+
}
|
|
730
|
+
output := {
|
|
731
|
+
@required
|
|
732
|
+
id: String
|
|
733
|
+
}
|
|
681
734
|
}
|
|
682
735
|
```
|
|
683
736
|
|
|
@@ -706,7 +759,7 @@ function ItemsAndUsers() {
|
|
|
706
759
|
<div>
|
|
707
760
|
<h2>Items</h2>
|
|
708
761
|
<ul>
|
|
709
|
-
{items.data?.map(item => (
|
|
762
|
+
{items.data?.items.map(item => (
|
|
710
763
|
<li key={item.id}>{item.name}</li>
|
|
711
764
|
))}
|
|
712
765
|
</ul>
|
|
@@ -714,7 +767,7 @@ function ItemsAndUsers() {
|
|
|
714
767
|
|
|
715
768
|
<h2>Users</h2>
|
|
716
769
|
<ul>
|
|
717
|
-
{users.data?.map(user => (
|
|
770
|
+
{users.data?.users.map(user => (
|
|
718
771
|
<li key={user.id}>{user.name}</li>
|
|
719
772
|
))}
|
|
720
773
|
</ul>
|
|
@@ -805,7 +858,7 @@ Define your error structures in your Smithy model:
|
|
|
805
858
|
|
|
806
859
|
```smithy
|
|
807
860
|
@error("client")
|
|
808
|
-
@httpError(
|
|
861
|
+
@httpError(422)
|
|
809
862
|
structure InvalidRequestError {
|
|
810
863
|
@required
|
|
811
864
|
message: String
|
|
@@ -842,6 +895,10 @@ structure FieldError {
|
|
|
842
895
|
}
|
|
843
896
|
```
|
|
844
897
|
|
|
898
|
+
:::caution[400 is taken]
|
|
899
|
+
Your service already declares Smithy's `ValidationException` on `400`, so every operation gets a `400` case carrying `ValidationExceptionResponseContent`. Give your own client errors a different status (`422` above) — two errors on the same status collide and only one payload reaches the generated client.
|
|
900
|
+
:::
|
|
901
|
+
|
|
845
902
|
#### Adding Errors to Operations
|
|
846
903
|
|
|
847
904
|
Specify which errors your operations can return:
|
|
@@ -884,37 +941,21 @@ import { useMutation, useQuery } from '@tanstack/react-query';
|
|
|
884
941
|
function ItemComponent() {
|
|
885
942
|
const api = useMyApi();
|
|
886
943
|
|
|
887
|
-
|
|
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
|
-
});
|
|
944
|
+
const getItem = useQuery(api.getItem.queryOptions({ itemId: '123' }));
|
|
905
945
|
|
|
906
946
|
// Mutation with typed error handling
|
|
907
947
|
const createItem = useMutation({
|
|
908
948
|
...api.createItem.mutationOptions(),
|
|
909
949
|
onError: (error) => {
|
|
950
|
+
// Error is typed based on the errors in your Smithy model
|
|
910
951
|
switch (error.status) {
|
|
911
|
-
case
|
|
912
|
-
// error.error is typed as
|
|
952
|
+
case 422:
|
|
953
|
+
// error.error is typed as InvalidRequestErrorResponseContent
|
|
913
954
|
console.error('Validation error:', error.error.message);
|
|
914
955
|
console.error('Field errors:', error.error.fieldErrors);
|
|
915
956
|
break;
|
|
916
957
|
case 403:
|
|
917
|
-
// error.error is typed as
|
|
958
|
+
// error.error is typed as UnauthorizedErrorResponseContent
|
|
918
959
|
console.error('Unauthorized:', error.error.reason);
|
|
919
960
|
break;
|
|
920
961
|
}
|
|
@@ -923,10 +964,13 @@ function ItemComponent() {
|
|
|
923
964
|
|
|
924
965
|
// Component rendering with error handling
|
|
925
966
|
if (getItem.isError) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
967
|
+
switch (getItem.error.status) {
|
|
968
|
+
case 404:
|
|
969
|
+
// error.error is typed as ItemNotFoundErrorResponseContent
|
|
970
|
+
return <NotFoundMessage message={getItem.error.error.message} />;
|
|
971
|
+
case 500:
|
|
972
|
+
// error.error is typed as InternalServerErrorResponseContent
|
|
973
|
+
return <ErrorMessage message={getItem.error.error.message} />;
|
|
930
974
|
}
|
|
931
975
|
}
|
|
932
976
|
|
|
@@ -962,11 +1006,11 @@ function ItemComponent() {
|
|
|
962
1006
|
|
|
963
1007
|
switch (err.status) {
|
|
964
1008
|
case 404:
|
|
965
|
-
// err.error is typed as
|
|
1009
|
+
// err.error is typed as ItemNotFoundErrorResponseContent
|
|
966
1010
|
console.error('Not found:', err.error.message);
|
|
967
1011
|
break;
|
|
968
1012
|
case 500:
|
|
969
|
-
// err.error is typed as
|
|
1013
|
+
// err.error is typed as InternalServerErrorResponseContent
|
|
970
1014
|
console.error('Server error:', err.error.message);
|
|
971
1015
|
console.error('Trace ID:', err.error.traceId);
|
|
972
1016
|
break;
|
|
@@ -987,13 +1031,13 @@ function ItemComponent() {
|
|
|
987
1031
|
const err = e as CreateItemError;
|
|
988
1032
|
|
|
989
1033
|
switch (err.status) {
|
|
990
|
-
case
|
|
991
|
-
// err.error is typed as
|
|
1034
|
+
case 422:
|
|
1035
|
+
// err.error is typed as InvalidRequestErrorResponseContent
|
|
992
1036
|
console.error('Validation error:', err.error.message);
|
|
993
1037
|
console.error('Field errors:', err.error.fieldErrors);
|
|
994
1038
|
break;
|
|
995
1039
|
case 403:
|
|
996
|
-
// err.error is typed as
|
|
1040
|
+
// err.error is typed as UnauthorizedErrorResponseContent
|
|
997
1041
|
console.error('Unauthorized:', err.error.reason);
|
|
998
1042
|
break;
|
|
999
1043
|
}
|
|
@@ -1037,7 +1081,7 @@ import { useQuery } from '@tanstack/react-query';
|
|
|
1037
1081
|
|
|
1038
1082
|
function ItemList() {
|
|
1039
1083
|
const api = useMyApi();
|
|
1040
|
-
const items = useQuery(api.listItems.queryOptions());
|
|
1084
|
+
const items = useQuery(api.listItems.queryOptions({}));
|
|
1041
1085
|
|
|
1042
1086
|
if (items.isLoading) {
|
|
1043
1087
|
return <LoadingSpinner />;
|
|
@@ -1047,11 +1091,10 @@ function ItemList() {
|
|
|
1047
1091
|
const err = items.error;
|
|
1048
1092
|
switch (err.status) {
|
|
1049
1093
|
case 403:
|
|
1050
|
-
// err.error is typed as
|
|
1094
|
+
// err.error is typed as UnauthorizedErrorResponseContent
|
|
1051
1095
|
return <ErrorMessage message={err.error.reason} />;
|
|
1052
1096
|
case 500:
|
|
1053
|
-
|
|
1054
|
-
// err.error is typed as ListItems5XXResponse
|
|
1097
|
+
// err.error is typed as InternalServerErrorResponseContent
|
|
1055
1098
|
return (
|
|
1056
1099
|
<ErrorMessage
|
|
1057
1100
|
message={err.error.message}
|
|
@@ -1064,7 +1107,7 @@ function ItemList() {
|
|
|
1064
1107
|
|
|
1065
1108
|
return (
|
|
1066
1109
|
<ul>
|
|
1067
|
-
{items.data.map((item) => (
|
|
1110
|
+
{items.data?.items.map((item) => (
|
|
1068
1111
|
<li key={item.id}>{item.name}</li>
|
|
1069
1112
|
))}
|
|
1070
1113
|
</ul>
|
|
@@ -1083,7 +1126,7 @@ function ItemList() {
|
|
|
1083
1126
|
useEffect(() => {
|
|
1084
1127
|
const fetchItems = async () => {
|
|
1085
1128
|
try {
|
|
1086
|
-
const data = await api.listItems();
|
|
1129
|
+
const data = await api.listItems({});
|
|
1087
1130
|
setItems(data);
|
|
1088
1131
|
} catch (err) {
|
|
1089
1132
|
setError(err);
|
|
@@ -1102,11 +1145,10 @@ function ItemList() {
|
|
|
1102
1145
|
const err = error as ListItemsError;
|
|
1103
1146
|
switch (err.status) {
|
|
1104
1147
|
case 403:
|
|
1105
|
-
// err.error is typed as
|
|
1148
|
+
// err.error is typed as UnauthorizedErrorResponseContent
|
|
1106
1149
|
return <ErrorMessage message={err.error.reason} />;
|
|
1107
1150
|
case 500:
|
|
1108
|
-
|
|
1109
|
-
// err.error is typed as ListItems5XXResponse
|
|
1151
|
+
// err.error is typed as InternalServerErrorResponseContent
|
|
1110
1152
|
return (
|
|
1111
1153
|
<ErrorMessage
|
|
1112
1154
|
message={err.error.message}
|
|
@@ -1134,41 +1176,53 @@ Implement optimistic updates for a better user experience:
|
|
|
1134
1176
|
|
|
1135
1177
|
```tsx
|
|
1136
1178
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
1179
|
+
import type { ListItemsResponseContent } from '../generated/my-api/types.gen';
|
|
1137
1180
|
|
|
1138
1181
|
function ItemList() {
|
|
1139
1182
|
const api = useMyApi();
|
|
1140
1183
|
const queryClient = useQueryClient();
|
|
1141
1184
|
|
|
1142
1185
|
// Query to fetch items
|
|
1143
|
-
const itemsQuery = useQuery(api.listItems.queryOptions());
|
|
1186
|
+
const itemsQuery = useQuery(api.listItems.queryOptions({}));
|
|
1144
1187
|
|
|
1145
1188
|
// Mutation for deleting items with optimistic updates
|
|
1146
1189
|
const deleteMutation = useMutation({
|
|
1147
1190
|
...api.deleteItem.mutationOptions(),
|
|
1148
|
-
onMutate: async (itemId) => {
|
|
1191
|
+
onMutate: async ({ itemId }) => {
|
|
1149
1192
|
// Cancel any outgoing refetches
|
|
1150
|
-
await queryClient.cancelQueries({
|
|
1193
|
+
await queryClient.cancelQueries({
|
|
1194
|
+
queryKey: api.listItems.queryKey({}),
|
|
1195
|
+
});
|
|
1151
1196
|
|
|
1152
1197
|
// Snapshot the previous value
|
|
1153
|
-
const previousItems = queryClient.getQueryData(
|
|
1198
|
+
const previousItems = queryClient.getQueryData<ListItemsResponseContent>(
|
|
1199
|
+
api.listItems.queryKey({}),
|
|
1200
|
+
);
|
|
1154
1201
|
|
|
1155
1202
|
// Optimistically update to the new value
|
|
1156
|
-
queryClient.setQueryData(
|
|
1157
|
-
api.listItems.queryKey(),
|
|
1158
|
-
(old) =>
|
|
1203
|
+
queryClient.setQueryData<ListItemsResponseContent>(
|
|
1204
|
+
api.listItems.queryKey({}),
|
|
1205
|
+
(old) =>
|
|
1206
|
+
old && {
|
|
1207
|
+
...old,
|
|
1208
|
+
items: old.items.filter((item) => item.id !== itemId),
|
|
1209
|
+
},
|
|
1159
1210
|
);
|
|
1160
1211
|
|
|
1161
1212
|
// Return a context object with the snapshot
|
|
1162
1213
|
return { previousItems };
|
|
1163
1214
|
},
|
|
1164
|
-
onError: (err,
|
|
1215
|
+
onError: (err, _input, context) => {
|
|
1165
1216
|
// If the mutation fails, use the context returned from onMutate to roll back
|
|
1166
|
-
queryClient.setQueryData(
|
|
1217
|
+
queryClient.setQueryData(
|
|
1218
|
+
api.listItems.queryKey({}),
|
|
1219
|
+
context?.previousItems,
|
|
1220
|
+
);
|
|
1167
1221
|
console.error('Failed to delete item:', err);
|
|
1168
1222
|
},
|
|
1169
1223
|
onSettled: () => {
|
|
1170
1224
|
// Always refetch after error or success to ensure data is in sync with server
|
|
1171
|
-
queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() });
|
|
1225
|
+
queryClient.invalidateQueries({ queryKey: api.listItems.queryKey({}) });
|
|
1172
1226
|
},
|
|
1173
1227
|
});
|
|
1174
1228
|
|
|
@@ -1182,11 +1236,11 @@ function ItemList() {
|
|
|
1182
1236
|
|
|
1183
1237
|
return (
|
|
1184
1238
|
<ul>
|
|
1185
|
-
{itemsQuery.data.map((item) => (
|
|
1239
|
+
{itemsQuery.data?.items.map((item) => (
|
|
1186
1240
|
<li key={item.id}>
|
|
1187
1241
|
{item.name}
|
|
1188
1242
|
<button
|
|
1189
|
-
onClick={() => deleteMutation.mutate(item.id)}
|
|
1243
|
+
onClick={() => deleteMutation.mutate({ itemId: item.id })}
|
|
1190
1244
|
disabled={deleteMutation.isPending}
|
|
1191
1245
|
>
|
|
1192
1246
|
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
|
|
@@ -1261,8 +1315,8 @@ function ItemForm() {
|
|
|
1261
1315
|
if (createItem.error) {
|
|
1262
1316
|
const error = createItem.error;
|
|
1263
1317
|
switch (error.status) {
|
|
1264
|
-
case
|
|
1265
|
-
// error.error is typed as
|
|
1318
|
+
case 422:
|
|
1319
|
+
// error.error is typed as InvalidRequestErrorResponseContent
|
|
1266
1320
|
return (
|
|
1267
1321
|
<FormError
|
|
1268
1322
|
message="Invalid input"
|
|
@@ -1270,10 +1324,10 @@ function ItemForm() {
|
|
|
1270
1324
|
/>
|
|
1271
1325
|
);
|
|
1272
1326
|
case 403:
|
|
1273
|
-
// error.error is typed as
|
|
1327
|
+
// error.error is typed as UnauthorizedErrorResponseContent
|
|
1274
1328
|
return <AuthError reason={error.error.reason} />;
|
|
1275
1329
|
default:
|
|
1276
|
-
// error.error is typed as
|
|
1330
|
+
// error.error is typed as InternalServerErrorResponseContent for 500, etc.
|
|
1277
1331
|
return <ServerError message={error.error.message} />;
|
|
1278
1332
|
}
|
|
1279
1333
|
}
|
|
@@ -1309,16 +1363,16 @@ function ItemForm() {
|
|
|
1309
1363
|
// ✅ Error type includes all possible error responses
|
|
1310
1364
|
const err = e as CreateItemError;
|
|
1311
1365
|
switch (err.status) {
|
|
1312
|
-
case
|
|
1313
|
-
// err.error is typed as
|
|
1366
|
+
case 422:
|
|
1367
|
+
// err.error is typed as InvalidRequestErrorResponseContent
|
|
1314
1368
|
console.error('Validation errors:', err.error.fieldErrors);
|
|
1315
1369
|
break;
|
|
1316
1370
|
case 403:
|
|
1317
|
-
// err.error is typed as
|
|
1371
|
+
// err.error is typed as UnauthorizedErrorResponseContent
|
|
1318
1372
|
console.error('Not authorized:', err.error.reason);
|
|
1319
1373
|
break;
|
|
1320
1374
|
case 500:
|
|
1321
|
-
// err.error is typed as
|
|
1375
|
+
// err.error is typed as InternalServerErrorResponseContent
|
|
1322
1376
|
console.error('Server error:', err.error.message);
|
|
1323
1377
|
break;
|
|
1324
1378
|
}
|
|
@@ -1329,7 +1383,7 @@ function ItemForm() {
|
|
|
1329
1383
|
// Error UI can use type narrowing to handle different error types
|
|
1330
1384
|
if (error) {
|
|
1331
1385
|
switch (error.status) {
|
|
1332
|
-
case
|
|
1386
|
+
case 422:
|
|
1333
1387
|
return (
|
|
1334
1388
|
<FormError
|
|
1335
1389
|
message="Invalid input"
|
|
@@ -63,8 +63,7 @@ The generator creates the following structure in your React application:
|
|
|
63
63
|
- QueryClientProvider.tsx TanStack React Query client provider
|
|
64
64
|
- hooks
|
|
65
65
|
- useSigV4.tsx Hook for signing HTTP requests with SigV4 (IAM only)
|
|
66
|
-
- use\<ApiName>.tsx
|
|
67
|
-
- use\<ApiName>Client.tsx A hook returning the vanilla tRPC client for direct API calls
|
|
66
|
+
- use\<ApiName>.tsx Exports both `use<ApiName>`, returning the tRPC options proxy for TanStack Query integration, and `use<ApiName>Client`, returning the vanilla tRPC client for direct API calls
|
|
68
67
|
|
|
69
68
|
</FileTree>
|
|
70
69
|
|
|
@@ -103,6 +102,8 @@ function MyComponent() {
|
|
|
103
102
|
};
|
|
104
103
|
|
|
105
104
|
if (isLoading) return <div>Loading...</div>;
|
|
105
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
106
|
+
if (!data) return null;
|
|
106
107
|
|
|
107
108
|
return (
|
|
108
109
|
<ul>
|
|
@@ -114,6 +115,8 @@ function MyComponent() {
|
|
|
114
115
|
}
|
|
115
116
|
```
|
|
116
117
|
|
|
118
|
+
`data` is `undefined` until the query resolves, so guard on it before dereferencing.
|
|
119
|
+
|
|
117
120
|
### Using the Vanilla tRPC Client
|
|
118
121
|
|
|
119
122
|
The `use<ApiName>Client` hook provides access to the [vanilla tRPC client](https://trpc.io/docs/client/vanilla), which is useful for imperative API calls and subscriptions:
|
|
@@ -157,6 +160,8 @@ function MyComponent() {
|
|
|
157
160
|
);
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
if (!data) return null;
|
|
164
|
+
|
|
160
165
|
return (
|
|
161
166
|
<ul>
|
|
162
167
|
{data.map((user) => (
|
|
@@ -278,7 +283,7 @@ function UserList() {
|
|
|
278
283
|
|
|
279
284
|
const users = useQuery(trpc.users.list.queryOptions());
|
|
280
285
|
|
|
281
|
-
if (users.
|
|
286
|
+
if (users.isPending) {
|
|
282
287
|
return <LoadingSpinner />;
|
|
283
288
|
}
|
|
284
289
|
|
|
@@ -338,7 +343,7 @@ function UserList() {
|
|
|
338
343
|
|
|
339
344
|
return (
|
|
340
345
|
<ul>
|
|
341
|
-
{users.map((user) => (
|
|
346
|
+
{users.data?.map((user) => (
|
|
342
347
|
<li key={user.id}>
|
|
343
348
|
{user.name}
|
|
344
349
|
<button onClick={() => deleteMutation.mutate(user.id)}>Delete</button>
|
|
@@ -366,7 +371,7 @@ function UserList() {
|
|
|
366
371
|
|
|
367
372
|
return (
|
|
368
373
|
<ul>
|
|
369
|
-
{users.map((user) => (
|
|
374
|
+
{users.data?.map((user) => (
|
|
370
375
|
<li key={user.id} onMouseEnter={() => prefetchUser(user.id)}>
|
|
371
376
|
<Link to={`/users/${user.id}`}>{user.name}</Link>
|
|
372
377
|
</li>
|
|
@@ -417,11 +422,13 @@ It is important to note that infinite queries can only be used for procedures wi
|
|
|
417
422
|
The integration provides complete end-to-end type safety. Your IDE will provide full autocompletion and type checking for all your API calls:
|
|
418
423
|
|
|
419
424
|
```tsx
|
|
425
|
+
import { useMutation } from '@tanstack/react-query';
|
|
426
|
+
|
|
420
427
|
function UserForm() {
|
|
421
428
|
const trpc = useMyApi();
|
|
422
429
|
|
|
423
430
|
// ✅ Input is fully typed
|
|
424
|
-
const createUser = trpc.users.create.
|
|
431
|
+
const createUser = useMutation(trpc.users.create.mutationOptions());
|
|
425
432
|
|
|
426
433
|
const handleSubmit = (data: CreateUserInput) => {
|
|
427
434
|
// ✅ Type error if input doesn't match schema
|