@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.
Files changed (72) hide show
  1. package/bin/aws-nx-mcp.js +90 -45
  2. package/docs/get_started/existing-project.mdx +7 -4
  3. package/docs/get_started/quick-start.mdx +58 -5
  4. package/docs/get_started/tutorials/dungeon-game/1.mdx +23 -20
  5. package/docs/get_started/tutorials/dungeon-game/2.mdx +11 -3
  6. package/docs/get_started/tutorials/dungeon-game/3.mdx +4 -0
  7. package/docs/get_started/tutorials/dungeon-game/4.mdx +7 -2
  8. package/docs/get_started/tutorials/dungeon-game/wrap-up.mdx +10 -1
  9. package/docs/guides/agentcore-gateway.mdx +4 -2
  10. package/docs/guides/agentcore-harness.mdx +2 -1
  11. package/docs/guides/astro-docs.mdx +25 -7
  12. package/docs/guides/connection/py-agent-a2a.mdx +2 -0
  13. package/docs/guides/connection/py-agent-dynamodb.mdx +1 -1
  14. package/docs/guides/connection/py-agent-gateway.mdx +3 -0
  15. package/docs/guides/connection/py-agent-mcp.mdx +18 -4
  16. package/docs/guides/connection/py-agent-rdb.mdx +5 -4
  17. package/docs/guides/connection/py-fast-api-dynamodb.mdx +1 -1
  18. package/docs/guides/connection/py-fast-api-rdb.mdx +7 -2
  19. package/docs/guides/connection/py-mcp-server-dynamodb.mdx +1 -1
  20. package/docs/guides/connection/py-mcp-server-rdb.mdx +6 -6
  21. package/docs/guides/connection/react-agui.mdx +34 -25
  22. package/docs/guides/connection/react-fastapi.mdx +114 -116
  23. package/docs/guides/connection/react-py-agent.mdx +4 -0
  24. package/docs/guides/connection/react-smithy.mdx +152 -98
  25. package/docs/guides/connection/react-trpc.mdx +13 -6
  26. package/docs/guides/connection/smithy-dynamodb.mdx +2 -8
  27. package/docs/guides/connection/smithy-rdb.mdx +3 -6
  28. package/docs/guides/connection/trpc-rdb.mdx +6 -6
  29. package/docs/guides/connection/ts-agent-a2a.mdx +4 -4
  30. package/docs/guides/connection/ts-agent-dynamodb.mdx +13 -11
  31. package/docs/guides/connection/ts-agent-gateway.mdx +2 -0
  32. package/docs/guides/connection/ts-agent-mcp.mdx +19 -7
  33. package/docs/guides/connection/ts-agent-rdb.mdx +2 -2
  34. package/docs/guides/connection/ts-mcp-server-dynamodb.mdx +11 -7
  35. package/docs/guides/connection/ts-mcp-server-rdb.mdx +4 -3
  36. package/docs/guides/docker-bundling.mdx +23 -3
  37. package/docs/guides/fastapi.mdx +16 -5
  38. package/docs/guides/license.mdx +30 -6
  39. package/docs/guides/nx-generator.mdx +11 -12
  40. package/docs/guides/py-agent.mdx +129 -54
  41. package/docs/guides/py-mcp-server.mdx +3 -1
  42. package/docs/guides/py-rdb.mdx +13 -4
  43. package/docs/guides/python-lambda-function.mdx +8 -8
  44. package/docs/guides/python-project.mdx +28 -25
  45. package/docs/guides/react-website-auth.mdx +8 -8
  46. package/docs/guides/react-website.mdx +46 -27
  47. package/docs/guides/runtime-config.mdx +24 -4
  48. package/docs/guides/security.mdx +1 -1
  49. package/docs/guides/terraform-project.mdx +8 -2
  50. package/docs/guides/trpc.mdx +96 -12
  51. package/docs/guides/ts-agent.mdx +17 -3
  52. package/docs/guides/ts-dcr-proxy.mdx +24 -6
  53. package/docs/guides/ts-lambda-function.mdx +7 -1
  54. package/docs/guides/ts-mcp-server.mdx +45 -15
  55. package/docs/guides/ts-nx-plugin.mdx +17 -7
  56. package/docs/guides/ts-rdb.mdx +9 -2
  57. package/docs/guides/ts-smithy-api.mdx +76 -7
  58. package/docs/guides/typescript-infrastructure.mdx +27 -11
  59. package/docs/guides/typescript-project.mdx +12 -5
  60. package/docs/guides/workspace.mdx +21 -9
  61. package/docs/snippets/api/type-safe-api-integrations.mdx +2 -0
  62. package/docs/snippets/connection/a2a-infrastructure.mdx +3 -0
  63. package/docs/snippets/connection/infra-project-prerequisite.mdx +8 -0
  64. package/docs/snippets/connection/strands-agent-rdb-ssl-requirements.mdx +1 -1
  65. package/docs/snippets/connection/ts-lambda-rdb-ssl-requirements.mdx +1 -1
  66. package/docs/snippets/connection/ts-mcp-server-rdb-ssl-requirements.mdx +1 -1
  67. package/docs/snippets/pdk-migration/faq/type-safe-api.mdx +1 -1
  68. package/docs/snippets/prerequisites.mdx +1 -1
  69. package/docs/snippets/required-prerequisites.mdx +1 -1
  70. package/package.json +1 -1
  71. package/src/init/schema.json +5 -0
  72. 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.isLoading) return <div>Loading...</div>;
147
- if (item.isError) return <div>Error: {item.error.message}</div>;
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.message}
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.isLoading) {
340
+ if (items.isPending) {
335
341
  return <LoadingSpinner />;
336
342
  }
337
343
 
338
344
  if (items.isError) {
339
- return <ErrorMessage message={items.error.message} />;
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 `<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.
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 400:
475
- // error.error is typed as CreateItem400Response
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 CreateItem403Response
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
- case 502:
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 400:
524
- // error.error is typed as CreateItem400Response
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 CreateItem403Response
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
- case 502:
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
- Then Apply the `@query` trait to your Smithy operation to force it to be treated as a query:
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 ListItems {
582
- input: ListItemsInput
583
- output: ListItemsOutput
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.listItems.queryOptions());
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 ListItems {
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 ListItems {
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: ListItemsInput
662
- output: ListItemsOutput
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: CreateItemInput
668
- output: CreateItemOutput
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: ListUsersInput
674
- output: ListUsersOutput
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: CreateUserInput
680
- output: CreateUserOutput
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(400)
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
- // 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
- });
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 400:
912
- // error.error is typed as InvalidRequestError
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 UnauthorizedError
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
- 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} />;
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 ItemNotFoundError
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 InternalServerError
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 400:
991
- // err.error is typed as InvalidRequestError
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 UnauthorizedError
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 ListItems403Response
1094
+ // err.error is typed as UnauthorizedErrorResponseContent
1051
1095
  return <ErrorMessage message={err.error.reason} />;
1052
1096
  case 500:
1053
- case 502:
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 ListItems403Response
1148
+ // err.error is typed as UnauthorizedErrorResponseContent
1106
1149
  return <ErrorMessage message={err.error.reason} />;
1107
1150
  case 500:
1108
- case 502:
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({ queryKey: api.listItems.queryKey() });
1193
+ await queryClient.cancelQueries({
1194
+ queryKey: api.listItems.queryKey({}),
1195
+ });
1151
1196
 
1152
1197
  // Snapshot the previous value
1153
- const previousItems = queryClient.getQueryData(api.listItems.queryKey());
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) => old.filter((item) => item.id !== itemId)
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, itemId, context) => {
1215
+ onError: (err, _input, context) => {
1165
1216
  // If the mutation fails, use the context returned from onMutate to roll back
1166
- queryClient.setQueryData(api.listItems.queryKey(), context.previousItems);
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 400:
1265
- // error.error is typed as InvalidRequestError
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 UnauthorizedError
1327
+ // error.error is typed as UnauthorizedErrorResponseContent
1274
1328
  return <AuthError reason={error.error.reason} />;
1275
1329
  default:
1276
- // error.error is typed as InternalServerError for 500, etc.
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 400:
1313
- // err.error is typed as InvalidRequestError
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 UnauthorizedError
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 InternalServerError
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 400:
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 A hook returning the tRPC options proxy for TanStack Query integration
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.isLoading) {
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.useMutation();
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