Migration depuis AWS PDK
Ce guide vous accompagne à travers un exemple de migration d’un projet AWS PDK vers le Nx Plugin for AWS, tout en fournissant des conseils généraux sur ce sujet.
Migrer vers le Nx Plugin for AWS offre les avantages suivants par rapport à PDK :
- Builds plus rapides
- Plus facile à utiliser (UI et CLI)
- Compatible avec le vibe-coding (essayez notre serveur MCP !)
- Technologies plus modernes
- Développement local d’API et de sites web
- Plus de contrôle (modifiez les fichiers générés pour correspondre à votre cas d’usage)
- Et plus encore !
Exemple de migration : Application de liste de courses
Section intitulée « Exemple de migration : Application de liste de courses »Dans ce guide, nous utiliserons l’Application de liste de courses du tutoriel PDK comme projet cible à migrer. Suivez les étapes de ce tutoriel pour créer le projet cible si vous souhaitez suivre vous-même.
L’application de liste de courses se compose des types de projets PDK suivants :
MonorepoTsProjectTypeSafeApiProjectCloudscapeReactTsWebsiteProjectInfrastructureTsProject
Créer l’espace de travail
Section intitulée « Créer l’espace de travail »Pour commencer, nous allons créer un nouvel espace de travail pour notre nouveau projet. Bien que plus extrême qu’une migration sur place, cette approche nous donne le résultat final le plus propre. Créer un espace de travail Nx équivaut à utiliser le MonorepoTsProject de PDK :
pnpm create @aws/nx-workspace@1.0.0-rc.47 shopping-list --iac=cdkyarn create @aws/nx-workspace@1.0.0-rc.47 shopping-list --iac=cdknpm create @aws/nx-workspace@1.0.0-rc.47 -- shopping-list --iac=cdkbun create @aws/nx-workspace@1.0.0-rc.47 shopping-list --iac=cdkOuvrez le répertoire shopping-list créé par cette commande dans votre IDE préféré.
Migrer l’API
Section intitulée « Migrer l’API »Le TypeSafeApiProject utilisé dans l’application de liste de courses a fait usage de :
- Smithy comme langage de modélisation
- TypeScript pour l’implémentation des opérations
- La génération de hooks TypeScript pour l’intégration avec un site web React
Nous pouvons donc utiliser le générateur ts#smithy-api pour fournir une fonctionnalité équivalente.
Générer une API Smithy TypeScript
Section intitulée « Générer une API Smithy TypeScript »Exécutez le générateur ts#api avec framework défini sur smithy pour configurer votre projet d’API dans packages/api :
pnpm nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactiveyarn nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactivenpx nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactivebunx nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
pnpm nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#api --name=api --framework=smithy --namespace=com.aws --auth=iam --no-interactive --dry-run- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#api - Remplissez les paramètres requis
- name: api
- framework: smithy
- namespace: com.aws
- auth: iam
- Cliquez sur
Generate
Vous remarquerez que cela génère un projet model, ainsi qu’un projet backend. Le projet model contient votre modèle Smithy, et backend contient votre implémentation serveur.
Le backend utilise le Smithy Server Generator for TypeScript. Nous explorerons cela plus en détail ci-dessous.
Migrer le modèle Smithy
Section intitulée « Migrer le modèle Smithy »Maintenant que nous avons la structure de base pour notre projet d’API Smithy, nous pouvons migrer le modèle :
-
Supprimez les fichiers Smithy d’exemple générés dans
packages/api/model/src -
Copiez votre modèle depuis le répertoire
packages/api/model/src/main/smithydu projet PDK vers le répertoirepackages/api/model/srcde votre nouveau projet. -
Mettez à jour le nom du service et l’espace de noms dans
smithy-build.jsonpour correspondre à l’application PDK :smithy-build.json "plugins": {"openapi": {"service": "com.aws#MyApi",... -
Mettez à jour le service dans
main.smithypour ajouter l’erreurValidationException, qui est requise lors de l’utilisation du Smithy TypeScript Server SDK.main.smithy use smithy.framework#ValidationException/// My Shopping List API@restJson1service MyApi {version: "1.0"operations: [GetShoppingListsPutShoppingListDeleteShoppingList]errors: [BadRequestErrorNotAuthorizedErrorInternalFailureErrorValidationException]} -
Ajoutez un fichier
extensions.smithyàpackages/api/model/srcoù nous définirons un trait qui fournit des informations de pagination au client généré :extensions.smithy $version: "2"namespace com.awsuse smithy.openapi#specificationExtension@trait@specificationExtension(as: "x-cursor")structure cursor {inputToken: Stringenabled: Boolean} -
Ajoutez le nouveau trait
@cursorà l’opérationGetShoppingListsdansget-shopping-lists.smithy:operations/get-shopping-lists.smithy @readonly@http(method: "GET", uri: "/shopping-list")@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize", items: "shoppingLists")@cursor(inputToken: "nextToken")@handler(language: "typescript")operation GetShoppingLists {input := with [PaginatedInputMixin] {@httpQuery("shoppingListId")shoppingListId: ShoppingListId}Toutes les opérations
@paginateddoivent également utiliser@cursorsi vous utilisez le générateur de client fourni par le Nx Plugin for AWS (via le générateurapi-connection). -
Enfin, supprimez le trait
@handlerde toutes les opérations car il n’est pas pris en charge par le Nx Plugin for AWS. En utilisantts#smithy-api, nous n’avons pas besoin des constructions CDK de fonction lambda auto-générées et des cibles de bundling générées par ce trait, car nous utilisons un seul bundle pour toutes les fonctions lambda.
À ce stade, exécutons une compilation pour vérifier nos modifications de modèle et nous assurer que nous avons du code serveur généré avec lequel travailler. Il y aura quelques échecs dans le projet backend (@shopping-list/api) mais nous les résoudrons ensuite.
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildMigrer les gestionnaires Lambda
Section intitulée « Migrer les gestionnaires Lambda »Vous pouvez considérer le projet api/backend comme quelque peu équivalent au projet api/handlers/typescript de Type Safe API.
L’une des principales différences entre Type Safe API et le générateur ts#smithy-api est que les gestionnaires sont implémentés en utilisant le Smithy Server Generator for TypeScript, plutôt que les wrappers de gestionnaires générés par Type Safe API (trouvés dans le projet api/generated/typescript/runtime).
Les gestionnaires lambda de l’application de liste de courses dépendent du package @aws-sdk/client-dynamodb, alors installons-le dans le projet @shopping-list/api :
pnpm add @aws-sdk/client-dynamodb --filter apiyarn workspace @shopping-list/api add @aws-sdk/client-dynamodbnpm install --legacy-peer-deps @aws-sdk/client-dynamodb -w packages/apibun add @aws-sdk/client-dynamodb --cwd packages/apiEnsuite, copions le fichier handlers/src/dynamo-client.ts du projet PDK vers backend/src/operations afin qu’il soit disponible pour nos gestionnaires.
Le générateur ts#smithy-api génère un exemple d’opération Echo. Puisque nous l’avons supprimée de notre modèle, supprimez le gestionnaire correspondant dans backend/src/operations/echo.ts. Nous enregistrerons nos opérations migrées dans service.ts plus bas.
Pour migrer les gestionnaires, vous pouvez suivre ces étapes générales :
-
Copiez le gestionnaire depuis le répertoire
packages/api/handlers/typescript/srcde votre projet PDK vers le répertoirepackages/api/backend/src/operationsde votre nouveau projet. -
Supprimez les imports
my-api-typescript-runtimeet importez plutôt le type d’opération depuis le TypeScript Server SDK généré, ainsi que leServiceContextpar exemple :import {deleteShoppingListHandler,DeleteShoppingListChainedHandlerFunction,INTERCEPTORS,Response,LoggingInterceptor,} from 'myapi-typescript-runtime';import { DeleteShoppingList as DeleteShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js'; -
Supprimez l’export du wrapper de gestionnaire
export const handler = deleteShoppingListHandler(...INTERCEPTORS,deleteShoppingList,); -
Mettez à jour la signature de votre gestionnaire d’opération pour utiliser le SSDK :
export const deleteShoppingList: DeleteShoppingListChainedHandlerFunction = async (request) => {export const DeleteShoppingList: DeleteShoppingListOperation<ServiceContext> = async (input, ctx) => { -
Remplacez l’utilisation du
LoggingInterceptorparctx.logger. (S’applique également aux intercepteurs de métriques et de traçage) :LoggingInterceptor.getLogger(request).info('...');ctx.logger.info('...'); -
Mettez à jour les références aux paramètres d’entrée. Puisque le SSDK fournit des types qui correspondent exactement à votre modèle Smithy (plutôt que de regrouper les paramètres de chemin/requête/en-tête séparément du paramètre de corps), mettez à jour toutes les références d’entrée en conséquence :
const shoppingListId = request.input.requestParameters.shoppingListId;const shoppingListId = input.shoppingListId; -
Supprimez l’utilisation de
Response. Nous retournons simplement des objets simples dans le SSDK.return Response.success({ shoppingListId });return { shoppingListId };Nous ne lançons plus ou ne retournons plus
Response, nous lançons plutôt les erreurs générées par le SSDK :throw Response.badRequest({ message: 'oh no' });return Response.badRequest({ message: 'oh no' });import { BadRequestError } from '../generated/ssdk/index.js';throw new BadRequestError({ message: 'oh no' }); -
Mettez à jour tous les imports pour utiliser la syntaxe ESM, notamment en ajoutant l’extension
.jsaux imports relatifs. -
Ajoutez l’opération à
service.tsservice.ts import { ServiceContext } from './context.js';import { MyApiService } from './generated/ssdk/index.js';import { DeleteShoppingList } from './operations/delete-shopping-list.js';import { GetShoppingLists } from './operations/get-shopping-lists.js';import { PutShoppingList } from './operations/put-shopping-list.js';// Register operations to the service hereexport const Service: MyApiService<ServiceContext> = {PutShoppingList,GetShoppingLists,DeleteShoppingList,};
Migration des gestionnaires de liste de courses
Delete Shopping List
import { DeleteItemCommand } from '@aws-sdk/client-dynamodb';import { deleteShoppingListHandler, DeleteShoppingListChainedHandlerFunction, INTERCEPTORS, Response, LoggingInterceptor,} from 'myapi-typescript-runtime';import { ddbClient } from './dynamo-client';
/** * Type-safe handler for the DeleteShoppingList operation */export const deleteShoppingList: DeleteShoppingListChainedHandlerFunction = async (request) => { LoggingInterceptor.getLogger(request).info( 'Start DeleteShoppingList Operation', );
const shoppingListId = request.input.requestParameters.shoppingListId; await ddbClient.send( new DeleteItemCommand({ TableName: 'shopping_list', Key: { shoppingListId: { S: shoppingListId, }, }, }), );
return Response.success({ shoppingListId, });};
/** * Entry point for the AWS Lambda handler for the DeleteShoppingList operation. * The deleteShoppingListHandler method wraps the type-safe handler and manages marshalling inputs and outputs */export const handler = deleteShoppingListHandler( ...INTERCEPTORS, deleteShoppingList,);import { DeleteItemCommand } from '@aws-sdk/client-dynamodb';import { ddbClient } from './dynamo-client.js';import { DeleteShoppingList as DeleteShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js';
/** * Type-safe handler for the DeleteShoppingList operation */export const DeleteShoppingList: DeleteShoppingListOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info( 'Start DeleteShoppingList Operation', );
const shoppingListId = input.shoppingListId; await ddbClient.send( new DeleteItemCommand({ TableName: 'shopping_list', Key: { shoppingListId: { S: shoppingListId!, }, }, }), );
return { shoppingListId, };};Get Shopping Lists
import { DynamoDBClient, QueryCommand, QueryCommandInput, ScanCommand, ScanCommandInput } from '@aws-sdk/client-dynamodb';import { getShoppingListsHandler, GetShoppingListsChainedHandlerFunction, INTERCEPTORS, Response, LoggingInterceptor, ShoppingList,} from 'myapi-typescript-runtime';import { ddbClient } from './dynamo-client';
/** * Type-safe handler for the GetShoppingLists operation */export const getShoppingLists: GetShoppingListsChainedHandlerFunction = async (request) => { LoggingInterceptor.getLogger(request).info('Start GetShoppingLists Operation');
const nextToken = request.input.requestParameters.nextToken; const pageSize = request.input.requestParameters.pageSize; const shoppingListId = request.input.requestParameters.shoppingListId; const commandInput: ScanCommandInput | QueryCommandInput = { TableName: 'shopping_list', ConsistentRead: true, Limit: pageSize, ExclusiveStartKey: nextToken ? fromToken(nextToken) : undefined, ...(shoppingListId ? { KeyConditionExpression: 'shoppingListId = :shoppingListId', ExpressionAttributeValues: { ':shoppingListId': { S: request.input.requestParameters.shoppingListId!, }, }, } : {}), }; const response = await ddbClient.send(shoppingListId ? new QueryCommand(commandInput) : new ScanCommand(commandInput));
return Response.success({ shoppingLists: (response.Items || []) .map<ShoppingList>(item => ({ shoppingListId: item.shoppingListId.S!, name: item.name.S!, shoppingItems: JSON.parse(item.shoppingItems.S || '[]'), })), nextToken: response.LastEvaluatedKey ? toToken(response.LastEvaluatedKey) : undefined, });};
/** * Decode a stringified token * @param token a token passed to the paginated request */const fromToken = <T>(token?: string): T | undefined => token ? (JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString()) as T) : undefined;
/** * Encode pagination details into an opaque stringified token * @param paginationToken pagination token details */const toToken = <T>(paginationToken?: T): string | undefined => paginationToken ? encodeURIComponent(Buffer.from(JSON.stringify(paginationToken)).toString('base64')) : undefined;
/** * Entry point for the AWS Lambda handler for the GetShoppingLists operation. * The getShoppingListsHandler method wraps the type-safe handler and manages marshalling inputs and outputs */export const handler = getShoppingListsHandler(...INTERCEPTORS, getShoppingLists);import { QueryCommand, QueryCommandInput, ScanCommand, ScanCommandInput } from '@aws-sdk/client-dynamodb';import { ddbClient } from './dynamo-client.js';import { GetShoppingLists as GetShoppingListsOperation, ShoppingList } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js';
/** * Type-safe handler for the GetShoppingLists operation */export const GetShoppingLists: GetShoppingListsOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info('Start GetShoppingLists Operation');
const nextToken = input.nextToken; const pageSize = input.pageSize; const shoppingListId = input.shoppingListId; const commandInput: ScanCommandInput | QueryCommandInput = { TableName: 'shopping_list', ConsistentRead: true, Limit: pageSize, ExclusiveStartKey: nextToken ? fromToken(nextToken) : undefined, ...(shoppingListId ? { KeyConditionExpression: 'shoppingListId = :shoppingListId', ExpressionAttributeValues: { ':shoppingListId': { S: input.shoppingListId!, }, }, } : {}), }; const response = await ddbClient.send(shoppingListId ? new QueryCommand(commandInput) : new ScanCommand(commandInput));
return { shoppingLists: (response.Items || []) .map<ShoppingList>(item => ({ shoppingListId: item.shoppingListId.S!, name: item.name.S!, shoppingItems: JSON.parse(item.shoppingItems.S || '[]'), })), nextToken: response.LastEvaluatedKey ? toToken(response.LastEvaluatedKey) : undefined, };};
/** * Decode a stringified token * @param token a token passed to the paginated request */const fromToken = <T>(token?: string): T | undefined => token ? (JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString()) as T) : undefined;
/** * Encode pagination details into an opaque stringified token * @param paginationToken pagination token details */const toToken = <T>(paginationToken?: T): string | undefined => paginationToken ? encodeURIComponent(Buffer.from(JSON.stringify(paginationToken)).toString('base64')) : undefined;Put Shopping List
import { randomUUID } from 'crypto';import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';import { putShoppingListHandler, PutShoppingListChainedHandlerFunction, INTERCEPTORS, Response, LoggingInterceptor,} from 'myapi-typescript-runtime';import { ddbClient } from './dynamo-client';
/** * Type-safe handler for the PutShoppingList operation */export const putShoppingList: PutShoppingListChainedHandlerFunction = async (request) => { LoggingInterceptor.getLogger(request).info('Start PutShoppingList Operation');
const shoppingListId = request.input.body.shoppingListId ?? randomUUID(); await ddbClient.send(new PutItemCommand({ TableName: 'shopping_list', Item: { shoppingListId: { S: shoppingListId, }, name: { S: request.input.body.name, }, shoppingItems: { S: JSON.stringify(request.input.body.shoppingItems || []), }, }, }));
return Response.success({ shoppingListId, });};
/** * Entry point for the AWS Lambda handler for the PutShoppingList operation. * The putShoppingListHandler method wraps the type-safe handler and manages marshalling inputs and outputs */export const handler = putShoppingListHandler(...INTERCEPTORS, putShoppingList);import { randomUUID } from 'crypto';import { PutItemCommand } from '@aws-sdk/client-dynamodb';import { ddbClient } from './dynamo-client.js';import { PutShoppingList as PutShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js';
/** * Type-safe handler for the PutShoppingList operation */export const PutShoppingList: PutShoppingListOperation<ServiceContext> = async (input, ctx) => { ctx.logger.info('Start PutShoppingList Operation');
const shoppingListId = input.shoppingListId ?? randomUUID(); await ddbClient.send(new PutItemCommand({ TableName: 'shopping_list', Item: { shoppingListId: { S: shoppingListId, }, name: { S: input.name!, }, shoppingItems: { S: JSON.stringify(input.shoppingItems || []), }, }, }));
return { shoppingListId, };};Nous avons généré le projet d’API Smithy avec le nom api initialement car nous voulions qu’il soit ajouté à packages/api pour la cohérence avec le projet PDK. Puisque notre API Smithy définit maintenant service MyApi au lieu de service Api, nous devons mettre à jour toutes les instances de getApiServiceHandler avec getMyApiServiceHandler.
Effectuez cette modification dans handler.ts :
import { getApiServiceHandler } from './generated/ssdk/index.js'; import { getMyApiServiceHandler } from './generated/ssdk/index.js';
process.env.POWERTOOLS_METRICS_NAMESPACE = 'Api';process.env.POWERTOOLS_SERVICE_NAME = 'Api';
const tracer = new Tracer();const logger = new Logger();const metrics = new Metrics();
const serviceHandler = getApiServiceHandler(Service); const serviceHandler = getMyApiServiceHandler(Service);Et dans local-server.ts :
import { getApiServiceHandler } from './generated/ssdk/index.js';import { getMyApiServiceHandler } from './generated/ssdk/index.js';
const PORT = 3001;
const tracer = new Tracer();const logger = new Logger();const metrics = new Metrics();
const serviceHandler = getApiServiceHandler(Service);const serviceHandler = getMyApiServiceHandler(Service);De plus, mettez à jour packages/api/backend/project.json et modifiez metadata.apiName en my-api :
"metadata": { "generator": "ts#smithy-api", "apiName": "api", "apiName": "my-api", "auth": "iam", "modelProject": "@shopping-list/api-model", "ports": [3001] },Vérifier avec une compilation
Section intitulée « Vérifier avec une compilation »Nous pouvons maintenant compiler le projet pour vérifier que la migration a fonctionné jusqu’à présent :
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildMigrer le site web
Section intitulée « Migrer le site web »Le CloudscapeReactTsWebsiteProject utilisé dans l’application de liste de courses configurait un site web React avec CloudScape et l’authentification Cognito intégrée.
Ce type de projet s’appuyait sur create-react-app, qui est maintenant obsolète. Pour migrer le site web dans ce guide, nous utiliserons le générateur ts#website, qui utilise des technologies plus modernes et supportées, notamment Vite.
Dans le cadre de la migration, nous passerons également de React Router configuré par PDK à TanStack Router, qui ajoute une sécurité de type supplémentaire au routage du site web.
Générer un site web React
Section intitulée « Générer un site web React »Exécutez le générateur ts#website avec framework défini sur react pour configurer votre projet de site web dans packages/website. Étant donné que l’application de liste de courses est construite avec des composants CloudScape, nous définissons également ux sur cloudscape (la valeur par défaut est shadcn) :
pnpm nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactiveyarn nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactivenpx nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactivebunx nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
pnpm nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#website --name=website --framework=react --ux=cloudscape --no-interactive --dry-run- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#website - Remplissez les paramètres requis
- name: website
- framework: react
- ux: cloudscape
- Cliquez sur
Generate
Ajouter l’authentification Cognito
Section intitulée « Ajouter l’authentification Cognito »Le générateur de site web React ci-dessus n’inclut pas l’authentification cognito par défaut comme CloudscapeReactTsWebsiteProject, elle est ajoutée explicitement via le générateur ts#website#auth.
pnpm nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactiveyarn nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactivenpx nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactivebunx nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
pnpm nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#website#auth --project=website --cognitoDomain=shopping-list --no-interactive --dry-run- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#website#auth - Remplissez les paramètres requis
- project: website
- cognitoDomain: shopping-list
- Cliquez sur
Generate
Cela ajoute des composants React qui gèrent les redirections appropriées pour garantir que les utilisateurs se connectent à l’aide de l’interface utilisateur hébergée Cognito. Cela ajoute également une construction CDK pour déployer les ressources Cognito dans packages/common/constructs, appelée UserIdentity.
Connecter le site web à l’API
Section intitulée « Connecter le site web à l’API »Dans PDK, vous pouviez passer les projets Projen fournis les uns aux autres pour déclencher la génération de code d’intégration. Cela était utilisé dans l’application de liste de courses pour configurer le site web afin qu’il puisse s’intégrer à l’API.
Avec le Nx Plugin for AWS, l’intégration d’API est prise en charge via le générateur connection. Ensuite, nous utilisons ce générateur pour que notre site web puisse invoquer notre API Smithy :
pnpm nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactiveyarn nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactivenpx nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactivebunx nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
pnpm nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactive --dry-runyarn nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactive --dry-runnpx nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactive --dry-runbunx nx g @aws/nx-plugin:connection --sourceProject=website --targetProject=api --no-interactive --dry-run- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - connection - Remplissez les paramètres requis
- sourceProject: website
- targetProject: api
- Cliquez sur
Generate
Cela génère les fournisseurs de clients nécessaires et les cibles de build pour que votre site web puisse appeler votre API via un client TypeScript généré.
Ajouter la dépendance AWS Northstar
Section intitulée « Ajouter la dépendance AWS Northstar »Le CloudscapeReactTsWebsiteProject incluait automatiquement une dépendance à @aws-northstar/ui qui est utilisée dans notre application de liste de courses, nous l’ajoutons donc au projet @shopping-list/website :
pnpm add @aws-northstar/ui --filter websiteyarn workspace @shopping-list/website add @aws-northstar/uinpm install --legacy-peer-deps @aws-northstar/ui -w packages/websitebun add @aws-northstar/ui --cwd packages/website@aws-northstar/ui intègre un composant d’éditeur de code qui dépend de ace-builds, en utilisant une importation spécifique à webpack que Vite ne peut pas résoudre. Étant donné que notre application de liste de courses n’utilise pas ce composant, nous l’excluons du bundle en l’ajoutant à la configuration external dans les options build existantes dans packages/website/vite.config.mts :
build: { outDir: '../../dist/packages/website/bundle', emptyOutDir: true, reportCompressedSize: true, commonjsOptions: { transformMixedEsModules: true, }, rollupOptions: { external: ['ace-builds/webpack-resolver'], }, },Déplacer les composants et les pages
Section intitulée « Déplacer les composants et les pages »L’application de liste de courses a un composant appelé CreateItem, et deux pages, ShoppingList et ShoppingLists. Nous allons les migrer vers le nouveau site web, en apportant quelques ajustements puisque nous utilisons TanStack Router et le générateur de code client TypeScript du Nx Plugin for AWS.
-
Copiez
packages/website/src/components/CreateItem/index.tsxdu projet PDK vers le même emplacement exact dans le nouveau projet. -
Copiez
packages/website/src/pages/ShoppingLists/index.tsxverspackages/website/src/routes/index.tsx, carShoppingListsest notre page d’accueil et nous utilisons le routage basé sur les fichiers avec TanStack router. -
Copiez
packages/website/src/pages/ShoppingList/index.tsxverspackages/website/src/routes/$shoppingListId.tsx, carShoppingListétait la page que nous voulons afficher sur la route/:shoppingListId.
Notez que vous aurez maintenant des erreurs de build visibles dans votre IDE, nous devrons apporter quelques modifications supplémentaires pour nous adapter au nouveau framework, décrites ci-dessous.
Migrer de React Router vers TanStack Router
Section intitulée « Migrer de React Router vers TanStack Router »Puisque nous utilisons le routage basé sur les fichiers, nous pouvons utiliser le serveur de développement local du site web pour gérer automatiquement la génération de la configuration de routage.
Commençons par démarrer le serveur de site web local :
pnpm nx dev websiteyarn nx dev websitenpx nx dev websitebunx nx dev websiteVous verrez quelques erreurs, mais le serveur de site web local devrait démarrer sur le port 4200, ainsi que le serveur d’API Smithy local sur le port 3001.
Suivez les étapes ci-dessous dans routes/index.tsx et routes/$shoppingListId.tsx pour migrer vers TanStack Router :
-
Ajoutez
createFileRoutepour enregistrer chaque route :import { createFileRoute } from "@tanstack/react-router";...export default ShoppingLists;export const Route = createFileRoute('/')({component: ShoppingLists,});import { createFileRoute } from "@tanstack/react-router";...export default ShoppingList;export const Route = createFileRoute('/$shoppingListId')({component: ShoppingList,});Après avoir enregistré le fichier, vous remarquerez que les erreurs de type avec l’appel à
createFileRouteont disparu. -
Remplacez le hook
useNavigate.Mettez à jour l’importation :
import { useNavigate } from 'react-router-dom';import { useNavigate } from '@tanstack/react-router';Mettez à jour les appels à la méthode
navigate(retournée paruseNavigate) pour passer les routes type-safe :navigate(`/${cell.shoppingListId}`);navigate({to: '/$shoppingListId',params: { shoppingListId: cell.shoppingListId },}); -
Remplacez le hook
useParams.Supprimez l’importation :
import { useParams } from 'react-router-dom';Mettez à jour les appels à
useParamsavec le hook fourni par laRoutecréée ci-dessus. Ils sont maintenant type-safe !const { shoppingListId } = useParams();const { shoppingListId } = Route.useParams();
Corriger les importations de composants
Section intitulée « Corriger les importations de composants »Puisque nos fichiers de route ne sont pas aussi profondément imbriqués dans l’arborescence de fichiers qu’ils l’étaient dans notre projet PDK, nous devons corriger l’importation de CreateItem dans routes/index.tsx et routes/$shoppingListId.tsx :
import CreateItem from "../../components/CreateItem";import CreateItem from "../components/CreateItem";Le AppLayoutContext est également fourni dans un emplacement légèrement différent dans notre nouveau projet :
import { AppLayoutContext } from "../../layouts/App";import { AppLayoutContext } from "../components/AppLayout";Migrer pour utiliser le nouveau client TypeScript généré
Section intitulée « Migrer pour utiliser le nouveau client TypeScript généré »Nous nous rapprochons maintenant ! Ensuite, nous devons migrer pour utiliser le client TypeScript fourni par le Nx Plugin for AWS, qui présente quelques améliorations par rapport à Type Safe API. Pour y parvenir, suivez les étapes ci-dessous
-
Importez le nouveau client et les types générés au lieu des anciens, par exemple :
import {ShoppingList,usePutShoppingList,useDeleteShoppingList,useGetShoppingLists,} from "myapi-typescript-react-query-hooks";import { ShoppingList } from "../generated/my-api/types.gen";import { useMyApi } from "../hooks/useMyApi";import { useInfiniteQuery, useMutation } from "@tanstack/react-query";Notez que
routes/$shoppingListId.tsximporte le typeShoppingListcomme_ShoppingList- dans ce fichier, nous devons faire de même, mais en important à nouveau depuistypes.gen.Notez également que nous importons les hooks pertinents directement depuis
@tanstack/react-query, car le client généré fournit des méthodes pour générer des options pour les hooks TanStack query, plutôt que des wrappers de hooks. -
Instanciez les nouveaux hooks TanStack Query, par exemple :
const getShoppingLists = useGetShoppingLists({ pageSize: PAGE_SIZE });const putShoppingList = usePutShoppingList();const deleteShoppingList = useDeleteShoppingList();const api = useMyApi();const getShoppingLists = useInfiniteQuery(api.getShoppingLists.infiniteQueryOptions({ pageSize: PAGE_SIZE },{ getNextPageParam: (p) => p.nextToken },),);const putShoppingList = useMutation(api.putShoppingList.mutationOptions());const deleteShoppingList = useMutation(api.deleteShoppingList.mutationOptions(),); -
Supprimez le wrapper
<operation>RequestContentpour les appels aux opérations qui acceptent des paramètres dans le corps de la requête :await putShoppingList.mutateAsync({putShoppingListRequestContent: {name: item,},});
Migrer de TanStack Query v4 vers v5
Section intitulée « Migrer de TanStack Query v4 vers v5 »Il reste quelques erreurs à corriger en raison des différences entre TanStack Query v4 (utilisé par PDK) et v5 que le générateur connection a ajouté :
-
Remplacez
isLoadingparisPendingpour les mutations, par exemple :putShoppingList.isLoadingputShoppingList.isPending -
L’application de liste de courses utilisait le
InfiniteQueryTablede@aws-northstar/uiqui attend un type de TanStack Query v4. Cela fonctionne en fait avec les requêtes infinies de v5, nous pouvons donc simplement supprimer l’erreur de type :<InfiniteQueryTablequery={getShoppingLists}query={getShoppingLists as any}
Visiter le site web local
Section intitulée « Visiter le site web local »Vous pouvez maintenant visiter le site web local à l’adresse http://localhost:4200/
Le site web devrait maintenant se charger puisque tout a été migré ! Étant donné que la seule infrastructure sur laquelle l’application de liste de courses s’appuie en plus de l’API, du site web et de l’identité est la table DynamoDB - si vous avez une table DynamoDB nommée shopping_list dans la région, et des informations d’identification AWS locales qui peuvent y accéder, le site web sera entièrement fonctionnel !
Sinon, ce n’est pas grave, nous allons migrer l’infrastructure ensuite.
Migration de la page de liste de courses
Page des listes de courses
/* eslint-disable @typescript-eslint/no-floating-promises */import { InfiniteQueryTable } from "@aws-northstar/ui/components";import { Button, Header, Link, SpaceBetween, TableProps,} from "@cloudscape-design/components";import { ShoppingList, usePutShoppingList, useDeleteShoppingList, useGetShoppingLists,} from "myapi-typescript-react-query-hooks";import { useContext, useEffect, useMemo, useState } from "react";import { useNavigate } from "react-router-dom";import CreateItem from "../../components/CreateItem";import { AppLayoutContext } from "../../layouts/App";
const PAGE_SIZE = 50;
/** * Component to render the ShoppingLists "/" route. */const ShoppingLists: React.FC = () => { const [visibleModal, setVisibleModal] = useState(false); const [selectedShoppingList, setSelectedShoppingList] = useState< ShoppingList[] >([]); const getShoppingLists = useGetShoppingLists({ pageSize: PAGE_SIZE }); const putShoppingList = usePutShoppingList(); const deleteShoppingList = useDeleteShoppingList(); const navigate = useNavigate(); const { setAppLayoutProps } = useContext(AppLayoutContext);
useEffect(() => { setAppLayoutProps({ contentType: "table", }); }, [setAppLayoutProps]);
const columnDefinitions = useMemo< TableProps.ColumnDefinition<ShoppingList>[] >( () => [ { id: "shoppingListId", isRowHeader: true, header: "Shopping List Id", cell: (cell) => ( <Link href={`/${cell.shoppingListId}`} onFollow={(e) => { e.preventDefault(); navigate(`/${cell.shoppingListId}`); }} > {cell.shoppingListId} </Link> ), }, { id: "name", header: "Name", cell: (cell) => cell.name, }, { id: "shoppingItems", header: "Shopping Items", cell: (cell) => `${cell.shoppingItems?.length || 0} Items.`, }, ], [navigate], );
return ( <> <CreateItem title="Create Shopping List" callback={async (item) => { await putShoppingList.mutateAsync({ putShoppingListRequestContent: { name: item, }, }); getShoppingLists.refetch(); }} isLoading={putShoppingList.isLoading} visibleModal={visibleModal} setVisibleModal={setVisibleModal} /> <InfiniteQueryTable query={getShoppingLists} itemsKey="shoppingLists" pageSize={PAGE_SIZE} selectionType="single" stickyHeader={true} selectedItems={selectedShoppingList} onSelectionChange={(e) => setSelectedShoppingList(e.detail.selectedItems) } header={ <Header variant="awsui-h1-sticky" actions={ <SpaceBetween size="xs" direction="horizontal"> <Button loading={deleteShoppingList.isLoading} data-testid="header-btn-delete" disabled={selectedShoppingList.length === 0} onClick={async () => { await deleteShoppingList.mutateAsync({ shoppingListId: selectedShoppingList![0].shoppingListId, }); setSelectedShoppingList([]); getShoppingLists.refetch(); }} > Delete </Button> <Button data-testid="header-btn-create" variant="primary" onClick={() => setVisibleModal(true)} > Create Shopping List </Button> </SpaceBetween> } > Shopping Lists </Header> } variant="full-page" columnDefinitions={columnDefinitions} /> </> );};
export default ShoppingLists;/* eslint-disable @typescript-eslint/no-floating-promises */import { InfiniteQueryTable } from "@aws-northstar/ui/components";import { Button, Header, Link, SpaceBetween, TableProps,} from "@cloudscape-design/components";import { useContext, useEffect, useMemo, useState } from "react";import { useNavigate } from "@tanstack/react-router";import CreateItem from "../components/CreateItem";import { AppLayoutContext } from "../components/AppLayout";import { createFileRoute } from "@tanstack/react-router";import { ShoppingList } from "../generated/my-api/types.gen";import { useMyApi } from "../hooks/useMyApi";import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
const PAGE_SIZE = 50;
/** * Component to render the ShoppingLists "/" route. */const ShoppingLists: React.FC = () => { const [visibleModal, setVisibleModal] = useState(false); const [selectedShoppingList, setSelectedShoppingList] = useState< ShoppingList[] >([]); const api = useMyApi(); const getShoppingLists = useInfiniteQuery( api.getShoppingLists.infiniteQueryOptions( { pageSize: PAGE_SIZE }, { getNextPageParam: (res) => res.nextToken }, ), ); const putShoppingList = useMutation(api.putShoppingList.mutationOptions()); const deleteShoppingList = useMutation( api.deleteShoppingList.mutationOptions(), ); const navigate = useNavigate(); const { setAppLayoutProps } = useContext(AppLayoutContext);
useEffect(() => { setAppLayoutProps({ contentType: "table", }); }, [setAppLayoutProps]);
const columnDefinitions = useMemo< TableProps.ColumnDefinition<ShoppingList>[] >( () => [ { id: "shoppingListId", isRowHeader: true, header: "Shopping List Id", cell: (cell) => ( <Link href={`/${cell.shoppingListId}`} onFollow={(e) => { e.preventDefault(); navigate({ to: '/$shoppingListId', params: { shoppingListId: cell.shoppingListId },}); }} > {cell.shoppingListId} </Link> ), }, { id: "name", header: "Name", cell: (cell) => cell.name, }, { id: "shoppingItems", header: "Shopping Items", cell: (cell) => `${cell.shoppingItems?.length || 0} Items.`, }, ], [navigate], );
return ( <> <CreateItem title="Create Shopping List" callback={async (item) => { await putShoppingList.mutateAsync({ name: item, }); getShoppingLists.refetch(); }} isLoading={putShoppingList.isPending} visibleModal={visibleModal} setVisibleModal={setVisibleModal} /> <InfiniteQueryTable query={getShoppingLists as any} itemsKey="shoppingLists" pageSize={PAGE_SIZE} selectionType="single" stickyHeader={true} selectedItems={selectedShoppingList} onSelectionChange={(e) => setSelectedShoppingList(e.detail.selectedItems) } header={ <Header variant="awsui-h1-sticky" actions={ <SpaceBetween size="xs" direction="horizontal"> <Button loading={deleteShoppingList.isPending} data-testid="header-btn-delete" disabled={selectedShoppingList.length === 0} onClick={async () => { await deleteShoppingList.mutateAsync({ shoppingListId: selectedShoppingList![0].shoppingListId, }); setSelectedShoppingList([]); getShoppingLists.refetch(); }} > Delete </Button> <Button data-testid="header-btn-create" variant="primary" onClick={() => setVisibleModal(true)} > Create Shopping List </Button> </SpaceBetween> } > Shopping Lists </Header> } variant="full-page" columnDefinitions={columnDefinitions} /> </> );};
export const Route = createFileRoute('/')({ component: ShoppingLists,});Page de liste de courses
/* eslint-disable @typescript-eslint/no-floating-promises */import { Board, BoardItem, BoardProps,} from "@cloudscape-design/board-components";import { Button, Container, ContentLayout, Header, SpaceBetween, Spinner,} from "@cloudscape-design/components";import { ShoppingList as _ShoppingList, usePutShoppingList, useGetShoppingLists,} from "myapi-typescript-react-query-hooks";import { useEffect, useState } from "react";import { useParams } from "react-router-dom";import CreateItem from "../../components/CreateItem";
type ListItem = { name: string };
/** * Component to render a singular Shopping List "/:shoppingListId" route. */const ShoppingList: React.FC = () => { const { shoppingListId } = useParams(); const [visibleModal, setVisibleModal] = useState(false); const getShoppingLists = useGetShoppingLists({ shoppingListId }); const putShoppingList = usePutShoppingList(); const shoppingList: _ShoppingList | undefined = getShoppingLists.data?.pages[0].shoppingLists[0]!; const [shoppingItems, setShoppingItems] = useState<BoardProps.Item<ListItem>[]>();
useEffect(() => { setShoppingItems( shoppingList?.shoppingItems?.map((i) => ({ id: i, definition: { minColumnSpan: 4 }, data: { name: i }, })), ); }, [shoppingList?.shoppingItems]);
return ( <ContentLayout header={ <Header variant="awsui-h1-sticky" actions={ <SpaceBetween size="xs" direction="horizontal"> <Button data-testid="header-btn-create" variant="primary" onClick={() => setVisibleModal(true)} > Add Item </Button> </SpaceBetween> } > Shopping list: {shoppingList?.name} </Header> } > <CreateItem isLoading={false} title="Add Item" callback={async (item) => { const items = [ ...(shoppingItems || []), { id: item, definition: { minColumnSpan: 4 }, data: { name: item }, }, ]; setShoppingItems(items); putShoppingList.mutate({ putShoppingListRequestContent: { name: shoppingList.name, shoppingListId: shoppingList.shoppingListId, shoppingItems: items.map((i) => i.data.name), }, }); }} visibleModal={visibleModal} setVisibleModal={setVisibleModal} /> <Container> {!shoppingList ? ( <Spinner /> ) : ( <Board<ListItem> onItemsChange={(event) => { const items = event.detail.items as BoardProps.Item<ListItem>[]; setShoppingItems(items); putShoppingList.mutate({ putShoppingListRequestContent: { name: shoppingList.name, shoppingListId: shoppingList.shoppingListId, shoppingItems: items.map((i) => i.data.name), }, }); }} items={shoppingItems || []} renderItem={(item, actions) => ( <BoardItem header={item.data.name} settings={ <Button iconName="close" variant="icon" onClick={actions.removeItem} /> } i18nStrings={{ dragHandleAriaLabel: "Drag handle", dragHandleAriaDescription: "Use Space or Enter to activate drag, arrow keys to move, Space or Enter to submit, or Escape to discard.", resizeHandleAriaLabel: "Resize handle", resizeHandleAriaDescription: "Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard.", }} /> )} i18nStrings={{ liveAnnouncementDndCommitted: () => "", liveAnnouncementDndDiscarded: () => "", liveAnnouncementDndItemInserted: () => "", liveAnnouncementDndItemReordered: () => "", liveAnnouncementDndItemResized: () => "", liveAnnouncementDndStarted: () => "", liveAnnouncementItemRemoved: () => "", navigationAriaLabel: "", navigationItemAriaLabel: () => "", }} empty={<></>} /> )} </Container> </ContentLayout> );};
export default ShoppingList;// routes/$shoppingListId.tsx/* eslint-disable @typescript-eslint/no-floating-promises */import { Board, BoardItem, BoardProps,} from "@cloudscape-design/board-components";import { Button, Container, ContentLayout, Header, SpaceBetween, Spinner,} from "@cloudscape-design/components";import { useEffect, useState } from "react";import CreateItem from "../components/CreateItem";import { createFileRoute } from "@tanstack/react-router";import { useMyApi } from "../hooks/useMyApi";import { useInfiniteQuery, useMutation } from "@tanstack/react-query";import { ShoppingList as _ShoppingList } from "../generated/my-api/types.gen";
type ListItem = { name: string };
/** * Component to render a singular Shopping List "/:shoppingListId" route. */const ShoppingList: React.FC = () => { const { shoppingListId } = Route.useParams(); const [visibleModal, setVisibleModal] = useState(false); const api = useMyApi(); const getShoppingLists = useInfiniteQuery( api.getShoppingLists.infiniteQueryOptions( { shoppingListId }, { getNextPageParam: (p) => p.nextToken }, ), ); const putShoppingList = useMutation(api.putShoppingList.mutationOptions()); const shoppingList: _ShoppingList | undefined = getShoppingLists.data?.pages?.[0]?.shoppingLists?.[0]; const [shoppingItems, setShoppingItems] = useState<BoardProps.Item<ListItem>[]>();
useEffect(() => { setShoppingItems( shoppingList?.shoppingItems?.map((i) => ({ id: i, definition: { minColumnSpan: 4 }, data: { name: i }, })), ); }, [shoppingList?.shoppingItems]);
return ( <ContentLayout header={ <Header variant="awsui-h1-sticky" actions={ <SpaceBetween size="xs" direction="horizontal"> <Button data-testid="header-btn-create" variant="primary" onClick={() => setVisibleModal(true)} > Add Item </Button> </SpaceBetween> } > Shopping list: {shoppingList?.name} </Header> } > <CreateItem isLoading={false} title="Add Item" callback={async (item) => { const items = [ ...(shoppingItems || []), { id: item, definition: { minColumnSpan: 4 }, data: { name: item }, }, ]; setShoppingItems(items); putShoppingList.mutate({ name: shoppingList?.name ?? 'my list', shoppingListId: shoppingList?.shoppingListId, shoppingItems: items.map((i) => i.data.name), }); }} visibleModal={visibleModal} setVisibleModal={setVisibleModal} /> <Container> {!shoppingList ? ( <Spinner /> ) : ( <Board<ListItem> onItemsChange={(event) => { const items = event.detail.items as BoardProps.Item<ListItem>[]; setShoppingItems(items); putShoppingList.mutate({ name: shoppingList.name, shoppingListId: shoppingList.shoppingListId, shoppingItems: items.map((i) => i.data.name), }); }} items={shoppingItems || []} renderItem={(item, actions) => ( <BoardItem header={item.data.name} settings={ <Button iconName="close" variant="icon" onClick={actions.removeItem} /> } i18nStrings={{ dragHandleAriaLabel: "Drag handle", dragHandleAriaDescription: "Use Space or Enter to activate drag, arrow keys to move, Space or Enter to submit, or Escape to discard.", resizeHandleAriaLabel: "Resize handle", resizeHandleAriaDescription: "Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard.", }} /> )} i18nStrings={{ liveAnnouncementDndCommitted: () => "", liveAnnouncementDndDiscarded: () => "", liveAnnouncementDndItemInserted: () => "", liveAnnouncementDndItemReordered: () => "", liveAnnouncementDndItemResized: () => "", liveAnnouncementDndStarted: () => "", liveAnnouncementItemRemoved: () => "", navigationAriaLabel: "", navigationItemAriaLabel: () => "", }} empty={<></>} /> )} </Container> </ContentLayout> );};
export const Route = createFileRoute('/$shoppingListId')({ component: ShoppingList,});Migrer l’infrastructure
Section intitulée « Migrer l’infrastructure »Le dernier projet que nous devons migrer pour notre application de liste de courses est le InfrastructureTsProject. Il s’agit d’un projet TypeScript CDK, pour lequel l’équivalent dans le Nx Plugin for AWS est le générateur ts#infra.
En plus des projets Projen, PDK fournissait également des constructs CDK dont ces projets dépendent. Nous allons également migrer l’application de liste de courses de ces constructs CDK, en faveur de ceux générés par le Nx Plugin for AWS.
Générer un projet d’infrastructure TypeScript CDK
Section intitulée « Générer un projet d’infrastructure TypeScript CDK »Exécutez le générateur ts#infra pour configurer votre projet d’infrastructure dans packages/infra :
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactiveyarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivenpx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivebunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactiveVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-run- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - ts#infra - Remplissez les paramètres requis
- name: infra
- Cliquez sur
Generate
Migrer l’infrastructure CDK
Section intitulée « Migrer l’infrastructure CDK »L’application de liste de courses PDK instanciait les constructs suivants dans la pile d’application CDK :
DatabaseConstructpour la table DynamoDB stockant les listes de coursesUserIdentitypour les ressources Cognito, importé directement depuis PDKMyApipour déployer l’API Smithy, qui utilisait le construct TypeScript CDK généré avec des intégrations type-safe, dépendant du construct CDKTypeSafeRestApide PDK sous le capot.Websitepour déployer le site Web, enveloppant le construct CDKStaticWebsitede PDK.
Ensuite, nous allons migrer chacun d’entre eux vers le nouveau projet.
Copier la pile d’application
Section intitulée « Copier la pile d’application »Copiez packages/infra/src/stacks/application-stack.ts de l’application de liste de courses PDK vers le même emplacement exact dans votre nouveau projet. Vous verrez quelques erreurs TypeScript que nous allons corriger ci-dessous.
Copier le construct Database
Section intitulée « Copier le construct Database »L’application de liste de courses PDK avait un construct Database dans packages/src/constructs/database.ts. Copiez-le vers le même emplacement exact dans votre nouveau projet.
Étant donné que le Nx Plugin for AWS utilise Checkov pour les tests de sécurité, qui est un peu plus strict que PDK Nag, nous devons également ajouter quelques suppressions :
import { suppressRules } from '@shopping-list/common-constructs';...suppressRules( this.shoppingListTable, ['CKV_AWS_28', 'CKV_AWS_119'], 'Backup and KMS key not required for this project',);Dans application-stack.ts, mettez à jour l’import pour le DatabaseConstruct afin d’utiliser la syntaxe ESM :
import { DatabaseConstruct } from '../constructs/database';import { DatabaseConstruct } from '../constructs/database.js';Migrer le construct UserIdentity
Section intitulée « Migrer le construct UserIdentity »Le construct UserIdentity peut généralement être remplacé sans modifications en ajustant les imports.
import { UserIdentity } from "@aws/pdk/identity";import { UserIdentity } from '@shopping-list/common-constructs';...const userIdentity = new UserIdentity(this, `${id}UserIdentity`);Notez que les constructs sous-jacents utilisés par le nouveau construct UserIdentity sont fournis directement depuis aws-cdk-lib, alors que PDK utilisait @aws-cdk/aws-cognito-identitypool-alpha.
Migrer le construct API
Section intitulée « Migrer le construct API »L’application de liste de courses PDK avait un construct dans constructs/apis/myapi.ts qui instanciait un construct CDK que Type Safe API générait à partir de votre modèle Smithy.
En plus de ce construct, puisque le projet PDK utilisait le trait @handler, des constructs CDK de fonction lambda générés étaient également générés.
Comme Type Safe API, le Nx Plugin for AWS fournit la type-safety pour les intégrations basées sur votre modèle Smithy, mais cela est réalisé d’une manière beaucoup plus simple et flexible. Au lieu de générer un construct CDK entier au moment de la construction, seules des « métadonnées » minimales sont générées, que le packages/common/constructs/src/app/apis/api.ts utilise de manière générique. Vous pouvez en savoir plus sur l’utilisation du construct dans le guide du générateur ts#smithy-api.
Suivez les étapes ci-dessous :
-
Instanciez le construct
Apidansapplication-stack.tsstacks/application-stack.ts import { MyApi } from "../constructs/apis/myapi";import { Api } from '@shopping-list/common-constructs';...const myapi = new MyApi(this, "MyApi", {databaseConstruct,userIdentity,});const api = new Api(this, 'MyApi', {integrations: Api.defaultIntegrations(this).build(),});Remarquez ici que nous utilisons
Api.defaultIntegrations(this).build()- le comportement par défaut est de créer une fonction lambda pour chaque opération dans notre API, ce qui est le même comportement que nous avions dansmyapi.ts. -
Accordez les permissions aux fonctions lambda pour accéder à la table DynamoDB.
Dans l’application de liste de courses PDK, le
DatabaseConsructétait passé àMyApi, et il gérait l’ajout des permissions pertinentes à chaque construct de fonction généré. Nous allons le faire directement dans le fichierapplication-stack.tsen accédant à la propriétéintegrationstype-safe du constructApi:stacks/application-stack.ts // Grant our lambda functions scoped access to call DynamodatabaseConstruct.shoppingListTable.grantReadData(api.integrations.getShoppingLists.handler,);[api.integrations.putShoppingList.handler,api.integrations.deleteShoppingList.handler,].forEach((f) => databaseConstruct.shoppingListTable.grantWriteData(f)); -
Accordez les permissions aux utilisateurs authentifiés pour invoquer l’API.
Dans le
myapi.tsde l’application PDK, les utilisateurs authentifiés recevaient également des permissions IAM pour invoquer l’API. Nous allons faire l’équivalent dansapplication-stack.ts:stacks/application-stack.ts api.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
Migrer le construct Website
Section intitulée « Migrer le construct Website »Enfin, nous ajoutons le construct Website de packages/common/constructs/src/app/static-websites/website.ts à application-stack.ts, car c’est l’équivalent du packages/infra/src/constructs/websites/website.ts de l’application de liste de courses PDK.
import { Website } from "../constructs/websites/website";import { Website } from '@shopping-list/common-constructs';...new Website(this, "Website", { userIdentity, myapi,});new Website(this, 'Website');Notez que nous ne passons pas l’identité ou l’API au site Web - la configuration d’exécution est gérée au sein de chaque construct fourni par le Nx Plugin for AWS, où UserIdentity et Api enregistrent les valeurs nécessaires, et Website gère le déploiement vers /runtime-config.json sur votre site Web statique.
Construisons maintenant le projet maintenant que nous avons migré toutes les parties pertinentes de la base de code vers notre nouveau projet.
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildDéployer
Section intitulée « Déployer »Maintenant que nous avons notre base de code entièrement migrée, nous pouvons envisager de la déployer. Il existe deux approches possibles à ce stade.
Toutes Nouvelles Ressources (Simple)
Section intitulée « Toutes Nouvelles Ressources (Simple) »L’approche la plus simple consiste à traiter cela comme une application complètement nouvelle, ce qui signifie que nous allons “recommencer” avec une nouvelle table DynamoDB et un nouveau Cognito User Pool - perdant ainsi tous les utilisateurs et leurs listes de courses. Pour cette approche, il suffit de :
-
Supprimer la table DynamoDB nommée
shopping_list -
Déployer la nouvelle application :
Terminal window pnpm nx deploy infra shopping-list-infra-sandbox/*Terminal window yarn nx deploy infra shopping-list-infra-sandbox/*Terminal window npx nx deploy infra shopping-list-infra-sandbox/*Terminal window bunx nx deploy infra shopping-list-infra-sandbox/*
🎉 Et c’est terminé ! 🎉
Migrer les Ressources Stateful Existantes sans Interruption (Plus Complexe)
Section intitulée « Migrer les Ressources Stateful Existantes sans Interruption (Plus Complexe) »En réalité, il est plus probable que vous souhaitiez migrer les ressources AWS existantes afin qu’elles soient gérées par la nouvelle base de code, tout en évitant toute interruption de service pour vos clients.
Pour notre application de liste de courses, les ressources stateful qui nous intéressent sont la table DynamoDB qui contient les listes de courses de nos utilisateurs, et le User Pool qui contient les détails de tous nos utilisateurs enregistrés. Notre plan de haut niveau sera de conserver ces deux ressources clés et de les déplacer afin qu’elles soient gérées par notre nouvelle stack, puis de mettre à jour le DNS pour pointer vers notre nouveau site web (et API si exposée aux clients).
-
Mettre à jour votre nouvelle application pour référencer les ressources existantes que vous souhaitez conserver.
Pour l’application de liste de courses, nous faisons cela pour la table DynamoDB
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Et pour le Cognito User Pool
packages/common/constructs/src/core/user-identity.ts this.userPool = this.createUserPool();this.userPool = UserPool.fromUserPoolId(this,'UserPool','<your-user-pool-id>',); -
Construire et déployer la nouvelle application :
Terminal window pnpm nx run-many --target buildTerminal window yarn nx run-many --target buildTerminal window npx nx run-many --target buildTerminal window bunx nx run-many --target buildTerminal window pnpm nx deploy infra shopping-list-infra-sandbox/*Terminal window yarn nx deploy infra shopping-list-infra-sandbox/*Terminal window npx nx deploy infra shopping-list-infra-sandbox/*Terminal window bunx nx deploy infra shopping-list-infra-sandbox/*Nous avons maintenant notre nouvelle application déployée référençant les ressources existantes, ne recevant pas encore de trafic.
-
Effectuer des tests d’intégration complets pour s’assurer que la nouvelle application fonctionne comme prévu. Pour l’application de liste de courses, charger le site web et vérifier que vous pouvez vous connecter et créer, afficher, modifier et supprimer des listes de courses.
-
Annuler les modifications qui référencent les ressources existantes dans votre nouvelle application, mais ne les déployez pas encore.
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Et pour le Cognito User Pool
packages/common/constructs/src/core/user-identity.ts this.userPool = this.createUserPool();this.userPool = UserPool.fromUserPoolId(this,'UserPool','<your-user-pool-id>',);Et ensuite exécuter une construction
Terminal window pnpm nx run-many --target buildTerminal window yarn nx run-many --target buildTerminal window npx nx run-many --target buildTerminal window bunx nx run-many --target build -
Utiliser
cdk importdans le dossierpackages/infrade votre nouvelle application pour voir quelles ressources nous serons invités à importer.New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --forceParcourir les invites en appuyant sur entrée. L’importation échouera car les ressources sont gérées par une autre stack - c’est attendu, nous avons juste fait cette étape pour confirmer quelles ressources nous devrons conserver. Vous verrez une sortie comme celle-ci :
Fenêtre de terminal shopping-list-infra-sandbox/Application/ApplicationUserIdentity/UserPool/smsRole/Resource (AWS::IAM::Role): enter RoleName (empty to skip)shopping-list-infra-sandbox/Application/ApplicationUserIdentity/UserPool/Resource (AWS::Cognito::UserPool): enter UserPoolId (empty to skip)shopping-list-infra-sandbox/Application/Database/ShoppingList/Resource (AWS::DynamoDB::Table): import with TableName=shopping_list (y/n) yCela nous indique qu’il y a en fait 3 ressources que nous devrons importer dans notre nouvelle stack.
-
Mettre à jour votre ancien projet PDK pour définir
RemovalPolicysurRETAINpour les ressources découvertes à l’étape précédente. Au moment de la rédaction, c’est la valeur par défaut pour le User Pool et la table DynamoDB, mais nous devons la mettre à jour pour le SMS Role que nous avons découvert ci-dessus :application-stack.ts const userIdentity = new UserIdentity(this, `${id}UserIdentity`, {userPool,});const smsRole = userIdentity.userPool.node.findAll().filter(c => CfnResource.isCfnResource(c) &&c.node.path.includes('/smsRole/'))[0] as CfnResource;smsRole.applyRemovalPolicy(RemovalPolicy.RETAIN); -
Déployer votre projet PDK afin que les politiques de suppression soient appliquées
PDK Application cd packages/infranpx projen deploy -
Consulter la console CloudFormation et enregistrer les valeurs qui vous ont été demandées lors de l’étape
cdk importci-dessus- L’ID du User Pool, par exemple
us-west-2_XXXXX - Le nom du SMS Role, par exemple
infra-sandbox-UserIdentityUserPoolsmsRoleXXXXXX
- L’ID du User Pool, par exemple
-
Mettre à jour votre projet PDK pour référencer les ressources existantes au lieu de les créer
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Et pour le Cognito User Pool
application-stack.ts const userPool = UserPool.fromUserPoolId(this,'UserPool','<your-user-pool-id>',);const userIdentity = new UserIdentity(this, `${id}UserIdentity`, {// PDK construct accepts UserPool not IUserPool, but this still works!userPool: userPool as any,}); -
Déployer à nouveau votre projet PDK, cela signifiera que les ressources ne sont plus gérées par la stack CloudFormation de notre projet PDK.
PDK Application cd packages/infranpx projen deploy -
Maintenant que les ressources ne sont plus gérées, nous pouvons exécuter
cdk importdans notre nouvelle application pour effectuer réellement l’importation :New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --forceEntrer les valeurs lorsque demandé, l’importation devrait se terminer avec succès.
-
Déployer à nouveau la nouvelle application pour s’assurer que toutes les modifications apportées à ces ressources existantes (maintenant gérées par votre nouvelle stack) sont effectuées :
Terminal window pnpm nx deploy infra shopping-list-infra-sandbox/*Terminal window yarn nx deploy infra shopping-list-infra-sandbox/*Terminal window npx nx deploy infra shopping-list-infra-sandbox/*Terminal window bunx nx deploy infra shopping-list-infra-sandbox/* -
Effectuer un test complet de votre nouvelle application à nouveau
-
Mettre à jour les enregistrements DNS pour pointer vers votre nouveau site web (et API si nécessaire).
Nous recommandons une approche progressive en utilisant le Weighted Routing de Route53, par lequel une fraction des requêtes est dirigée vers la nouvelle application au début. Au fur et à mesure que vous surveillez vos métriques, vous pouvez augmenter le poids pour la nouvelle application jusqu’à ce qu’aucun trafic ne soit envoyé vers votre ancienne application PDK.
Si vous n’avez pas de DNS et que vous avez utilisé les domaines générés automatiquement pour le site web et l’API, vous pouvez toujours envisager de proxifier les requêtes (par exemple via une origine HTTP CloudFront ou des intégrations HTTP API Gateway).
-
Surveiller les métriques de l’application PDK pour s’assurer qu’il n’y a pas de trafic, et enfin détruire l’ancienne stack CloudFormation :
Fenêtre de terminal cd packages/infranpx projen destroy
C’était un peu plus complexe, mais nous avons réussi à migrer nos utilisateurs de manière transparente vers la nouvelle application ! 🎉🎉🎉
Nous bénéficions maintenant des nouveaux avantages du Nx Plugin for AWS par rapport à PDK :
- Des builds plus rapides
- Support du développement local de l’API
- Une base de code adaptée au vibe-coding (essayez notre serveur MCP !)
- Code client/serveur type-safe plus intuitif
- Et plus encore !
Frequently Asked Questions
Section intitulée « Frequently Asked Questions »Cette section fournit des conseils pour les fonctionnalités de PDK qui ne sont pas couvertes par l’exemple de migration ci-dessus.
En règle générale lors du passage de PDK, nous recommandons de commencer tout projet avec un espace de travail Nx, compte tenu de ses similitudes avec le Monorepo PDK. Nous recommandons également d’utiliser nos générateurs comme primitives sur lesquelles construire de nouveaux types.
pnpm create @aws/nx-workspace my-projectyarn create @aws/nx-workspace my-projectnpm create @aws/nx-workspace -- my-projectbun create @aws/nx-workspace my-projectCDK Graph
Section intitulée « CDK Graph »CDK Graph construit des graphes de vos ressources CDK connectées, et fournissait deux plugins :
Plugin Diagram
Section intitulée « Plugin Diagram »Le CDK Graph Diagram Plugin génère des diagrammes d’architecture AWS à partir de votre infrastructure CDK.
Pour une approche déterministe similaire, une alternative viable est CDK-Dia.
Avec les progrès de l’IA générative, de nombreux modèles de fondation sont capables de créer des diagrammes de haute qualité à partir de votre infrastructure CDK. Nous recommandons d’essayer le AWS Diagram MCP Server. Consultez cet article de blog pour une présentation détaillée.
Plugin Threat Composer
Section intitulée « Plugin Threat Composer »Le CDK Graph Threat Composer Plugin génère un Threat Composer de démarrage à partir de votre code CDK.
Ce plugin fonctionnait en filtrant simplement un modèle de menace de base contenant des exemples de menaces, et en les filtrant en fonction des ressources utilisées par votre stack.
Si vous êtes intéressé par ces exemples de menaces spécifiques, vous pouvez copier et filtrer le modèle de menace de base, ou l’utiliser comme contexte pour aider un modèle de fondation à en générer un similaire.
AWS Arch
Section intitulée « AWS Arch »AWS Arch fournissait des mappages entre les ressources CloudFormation et leurs icônes d’architecture associées pour CDK Graph ci-dessus.
Consultez la page AWS Architecture Icons pour les ressources liées aux icônes. Diagrams fournit également un moyen de créer des diagrammes sous forme de code.
Si vous utilisiez cela directement, envisagez de forker le projet et d’en prendre la responsabilité !
Pipeline
Section intitulée « Pipeline »PDK fournissait un PDKPipelineProject qui configurait un projet d’infrastructure CDK et utilisait un construct CDK qui encapsulait certaines ressources CDK Pipelines.
Pour migrer depuis cela, vous pouvez utiliser les constructs CDK Pipelines directement. En pratique, cependant, il est probablement plus simple d’utiliser quelque chose comme GitHub actions ou GitLab CI/CD, où vous définissez des CDK Stages et exécutez la commande de déploiement pour le stage approprié directement.
PDK Nag encapsule CDK Nag, et fournit un ensemble de règles spécifiques à la création de prototypes.
Pour migrer depuis PDK Nag, utilisez CDK Nag directement. Si vous avez besoin du même ensemble de règles, vous pouvez créer votre propre “pack” en suivant la documentation ici.
Type Safe API
Section intitulée « Type Safe API »Les composants les plus couramment utilisés de Type Safe API sont couverts dans l’exemple de migration ci-dessus, cependant il existe d’autres fonctionnalités, pour lesquelles les détails de migration sont ci-dessous.
APIs modélisées avec OpenAPI
Section intitulée « APIs modélisées avec OpenAPI »Le Nx Plugin for AWS prend en charge les APIs modélisées en Smithy, mais pas celles modélisées directement en OpenAPI. Le générateur ts#smithy-api est un bon point de départ que vous pouvez ensuite modifier. Vous pouvez définir votre spécification OpenAPI dans le dossier src du projet model au lieu de Smithy, et modifier le build.Dockerfile pour utiliser votre outil de génération de code souhaité pour les clients/serveurs s’ils ne sont pas disponibles sur NPM. Si vos outils souhaités sont sur NPM, vous pouvez simplement les installer en tant que dépendances de développement dans votre espace de travail Nx et les appeler directement en tant que cibles de build Nx.
Pour les backends type-safe modélisés en OpenAPI, vous pouvez envisager d’utiliser l’un des générateurs de serveur OpenAPI Generator. Ceux-ci ne génèrent pas directement pour AWS Lambda, mais vous pouvez utiliser l’AWS Lambda Web Adapter pour combler le fossé pour beaucoup d’entre eux.
Pour les clients TypeScript, vous pouvez utiliser le générateur ts#website et le générateur connection avec un exemple ts#api (avec framework défini sur smithy) pour voir comment les clients sont générés et intégrés avec un site web. Cela configure des cibles de build qui génèrent des clients en invoquant nos générateurs open-api#ts-client ou open-api#ts-hooks. Vous pouvez utiliser ces générateurs vous-même en les pointant vers votre spécification OpenAPI.
Pour d’autres langages, vous pouvez également voir si l’un des générateurs d’OpenAPI Generator répond à vos besoins.
Vous pouvez également créer un générateur sur mesure en utilisant le générateur ts#nx-generator. Référez-vous à la documentation de ce générateur pour plus de détails sur la façon de générer du code à partir d’OpenAPI. Vous pouvez utiliser les templates du Nx Plugin for AWS comme point de départ. Vous pouvez également vous référer aux templates de la base de code PDK pour plus d’inspiration, en notant que la structure de données sur laquelle les templates opèrent est un peu différente du Nx Plugin for AWS.
APIs modélisées avec TypeSpec
Section intitulée « APIs modélisées avec TypeSpec »Pour TypeSpec, la section ci-dessus pour OpenAPI s’applique également. Vous pouvez commencer par générer un ts#smithy-api, installer le compilateur TypeSpec et les packages OpenAPI dans votre espace de travail Nx, et mettre à jour la cible compile du projet model pour exécuter tsp compile à la place, en vous assurant qu’il génère une spécification OpenAPI dans le répertoire dist.
L’approche recommandée serait d’utiliser le générateur de serveur HTTP TypeSpec pour JavaScript pour générer votre code serveur, car cela fonctionne directement sur votre modèle TypeSpec.
Vous pouvez utiliser l’AWS Lambda Web Adapter pour exécuter le serveur généré sur AWS Lambda.
Vous pouvez également utiliser n’importe laquelle des options OpenAPI ci-dessus.
TypeSpec a ses propres générateurs de code pour les clients dans les trois langages pris en charge par Type Safe API :
La section OpenAPI ci-dessus s’applique également puisque TypeSpec peut compiler vers OpenAPI.
APIs modélisées avec Smithy
Section intitulée « APIs modélisées avec Smithy »L’exemple de migration ci-dessus décrit la migration pour utiliser le générateur ts#smithy-api. Cette section couvre les options pour les backends et clients Python et Java.
Le générateur de code Smithy pour Java. Celui-ci dispose d’un générateur de serveur Java ainsi que d’un adaptateur pour exécuter le serveur Java généré sur AWS Lambda.
Smithy n’a pas de générateur de serveur pour Python, vous devrez donc passer par OpenAPI. Référez-vous à la section ci-dessus concernant les APIs modélisées avec OpenAPI pour les options potentielles.
Le générateur de code Smithy pour Java. Celui-ci dispose d’un générateur de client Java.
Pour les clients Python, vous pouvez consulter Smithy Python.
Pour TypeScript, consultez Smithy TypeScript, ou utilisez la même approche que nous avons adoptée dans ts#smithy-api en passant par OpenAPI (nous avons opté pour cela car cela nous donne une cohérence entre les APIs tRPC, FastAPI et Smithy via les hooks TanStack Query).
Bibliothèque de formes Smithy
Section intitulée « Bibliothèque de formes Smithy »Type Safe API fournissait un type de projet Projen nommé SmithyShapeLibraryProject qui configurait un projet contenant des modèles Smithy pouvant être réutilisés par plusieurs APIs basées sur Smithy.
L’équivalent est le générateur smithy#project avec type défini sur shapes :
pnpm nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesyarn nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesnpx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesbunx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapesVous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
pnpm nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-runyarn nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-runnpx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-runbunx nx g @aws/nx-plugin:smithy#project --name=my-shapes --type=shapes --dry-run- Installez le Nx Console VSCode Plugin si ce n'est pas déjà fait
- Ouvrez la console Nx dans VSCode
- Cliquez sur
Generate (UI)dans la section "Common Nx Commands" - Recherchez
@aws/nx-plugin - smithy#project - Remplissez les paramètres requis
- name: my-shapes
- type: shapes
- Cliquez sur
Generate
Déplacez les formes de votre SmithyShapeLibraryProject dans le dossier src du projet généré, puis référez-vous au guide du projet Smithy pour savoir comment connecter la bibliothèque en tant que dépendance du modèle de votre API.
Intercepteurs
Section intitulée « Intercepteurs »Type Safe API fournissait les intercepteurs par défaut suivants :
- Intercepteurs de journalisation, de traçage et de métriques utilisant Powertools for AWS Lambda
- Intercepteur try-catch pour gérer les exceptions non capturées
- Intercepteur CORS pour retourner les en-têtes CORS
Le générateur ts#smithy-api instrumente la journalisation, le traçage et les métriques avec Powertools for AWS Lambda en utilisant Middy. Le comportement de l’intercepteur try-catch est intégré au Smithy TypeScript SSDK, et les en-têtes CORS sont ajoutés dans handler.ts.
Pour les intercepteurs de journalisation, de traçage et de métriques dans n’importe quel langage, utilisez directement Powertools for AWS Lambda.
Pour migrer les intercepteurs personnalisés, nous recommandons d’utiliser les bibliothèques suivantes :
- TypeScript - Middy
- Python - Powertools for AWS Lambda Middleware Factory
- Java - Instrumentez les méthodes avant/après votre logique métier en utilisant aws-lambda-java-libs pour une approche simple, ou envisagez AspectJ pour construire votre middleware sous forme d’annotations.
Génération de documentation
Section intitulée « Génération de documentation »Type Safe API fournissait la génération de documentation en utilisant Redocly CLI. Ceci est très facile à ajouter à un projet existant une fois que vous l’avez migré comme ci-dessus.
-
Installez le Redocly CLI
Terminal window pnpm add -Dw @redocly/cliTerminal window yarn add -D @redocly/cliTerminal window npm install --legacy-peer-deps -D @redocly/cliTerminal window bun add -D @redocly/cli -
Ajoutez une cible de génération de documentation à votre projet
modelen utilisantredocly build-docs, par exemple :model/project.json {..."documentation": {"cache": true,"outputs": ["{workspaceRoot}/dist/{projectRoot}/documentation"],"executor": "nx:run-commands","options": {"command": "redocly build-docs dist/packages/api/model/build/openapi/openapi.json --output=dist/packages/api/model/documentation/index.html","cwd": "{workspaceRoot}"},"dependsOn": ["compile"]}}
Vous pouvez également envisager les générateurs de documentation OpenAPI Generator.
Intégrations simulées
Section intitulée « Intégrations simulées »Type Safe API générait des simulations pour vous dans son package d’infrastructure généré.
Vous pouvez passer à JSON Schema Faker qui peut créer les données simulées basées sur des schémas JSON. Cela peut fonctionner directement sur une spécification OpenAPI, et dispose d’une CLI que vous pourriez exécuter dans le cadre de la build de votre projet model.
Vous pouvez mettre à jour votre infrastructure CDK pour lire le fichier JSON généré par JSON Schema Faker, et retourner l’MockIntegration API Gateway appropriée pour une intégration, basée sur le metadata.gen.ts généré (en supposant que vous avez utilisé le générateur ts#smithy-api).
Backends multi-langages
Section intitulée « Backends multi-langages »Type Safe API prenait en charge l’implémentation d’APIs avec un mélange de différents langages dans le backend. Cela peut également être réalisé en fournissant des “overrides” aux intégrations lors de l’instanciation de votre construct API dans CDK :
const pythonLambdaHandler = new Function(this, 'PythonImplementation', { runtime: Runtime.PYTHON_3_12, ...});
new MyApi(this, 'MyApi', { integrations: Api.defaultIntegrations(this) .withOverrides({ echo: { integration: new LambdaIntegration(pythonLambdaHandler), handler: pythonLambdaHandler, }, }) .build(),});Vous devrez “stubber” votre service/routeur pour que votre service compile si vous utilisez le ts#smithy-api et le Server SDK TypeScript, par exemple :
export const Service: ApiService<ServiceContext> = { ... Echo: () => { throw new Error(`Not Implemented`); },};Validation des entrées
Section intitulée « Validation des entrées »Type Safe API ajoutait la validation native API Gateway pour les corps de requête basée sur votre spécification OpenAPI car elle utilisait le construct SpecRestApi en coulisses.
Avec le générateur ts#smithy-api, la validation est effectuée par le Server SDK lui-même. C’est la même chose pour la plupart des générateurs de serveur.
Si vous souhaitez implémenter la validation native API Gateway, vous pourriez le faire en modifiant packages/common/constructs/src/core/api/rest-api.ts pour lire le schéma JSON pertinent pour le corps de requête de chaque opération à partir de votre spécification OpenAPI.
APIs WebSocket
Section intitulée « APIs WebSocket »Malheureusement, il n’y a pas de chemin de migration simple pour l’API websocket de Type Safe API utilisant API Gateway et Lambda avec le développement d’API piloté par modèle. Cependant, cette section du guide vise au moins à offrir quelques idées.
Envisagez d’utiliser AsyncAPI pour modéliser votre API au lieu d’OpenAPI ou TypeSpec car cela est conçu pour gérer les APIs asynchrones. Le template NodeJS AsyncAPI peut générer un backend websocket Node que vous pourriez héberger sur ECS par exemple.
Vous pouvez également envisager AppSync Events pour l’infrastructure, et utiliser Powertools. Cet article de blog vaut la peine d’être lu !
Une autre option est d’utiliser des APIs GraphQL avec des websockets sur AppSync, pour lesquelles nous avons un problème GitHub que vous pouvez +1 ! Référez-vous au guide du développeur AppSync pour plus de détails et des liens vers des exemples de projets.
Vous pouvez également envisager de créer vos propres générateurs de code qui interprètent les mêmes extensions de fournisseur que Type Safe API. Référez-vous à la section APIs modélisées avec OpenAPI pour plus de détails sur la création de générateurs de code personnalisés basés sur OpenAPI. Vous pouvez trouver les templates que Type Safe API utilise pour les gestionnaires Lambda API Gateway Websocket API ici, et le client ici.
Vous pouvez également envisager de migrer pour utiliser le générateur ts#trpc-api pour utiliser tRPC. Au moment de la rédaction, nous n’avons pas encore de support pour les subscriptions/streaming mais si c’est quelque chose dont vous avez besoin, ajoutez un +1 à notre problème GitHub qui suit cela.
Smithy est agnostique au protocole, mais n’a pas encore de support pour le protocole Websocket, référez-vous à ce problème GitHub qui suit le support.
Infrastructure in Python or Java
Section intitulée « Infrastructure in Python or Java »PDK prenait en charge l’infrastructure CDK écrite en Python et Java. Nous ne prenons pas en charge cela dans le Nx Plugin for AWS au moment de la rédaction.
Le chemin recommandé serait soit de migrer votre infrastructure CDK vers TypeScript, soit d’utiliser nos générateurs et de migrer le package de constructs communs vers le langage de votre choix. Vous pouvez utiliser l’IA générative pour accélérer ce type de migrations, par exemple Kiro CLI. Vous pouvez faire itérer un agent IA sur la migration jusqu’à ce que les modèles CloudFormation synthétisés soient identiques.
Il en va de même pour l’infrastructure générée par Type Safe API en Python ou Java - vous pouvez traduire le construct générique rest-api.ts du package de constructs communs, et implémenter votre propre générateur de métadonnées simple pour votre langage cible (référez-vous à la section APIs Modelled with OpenAPI).
Vous pouvez utiliser le générateur py#project pour un projet Python de base auquel ajouter votre code CDK (et déplacer votre fichier cdk.json, en ajoutant les targets pertinents). Vous pouvez utiliser le plugin @nx/gradle de Nx pour les projets Java, ou @jnxplus/nx-maven pour Maven.
Use of Projen
Section intitulée « Use of Projen »PDK a été construit sur Projen. Projen et Nx Generators ont des différences assez fondamentales, ce qui signifie que bien qu’il soit techniquement possible de les combiner, c’est probablement un anti-pattern. Projen gère les fichiers de projet sous forme de code de sorte qu’ils ne peuvent pas être modifiés directement, tandis que les générateurs Nx fournissent les fichiers de projet une seule fois, puis le code peut être librement modifié.
Si vous souhaitez continuer à utiliser Projen, vous pouvez implémenter vous-même les types de projets Projen souhaités. Pour suivre les modèles du Nx Plugin for AWS, vous pouvez exécuter nos générateurs ou examiner leur code source sur GitHub pour voir comment vos types de projets souhaités sont construits, et implémenter les parties pertinentes en utilisant les primitives de Projen.