Migración desde AWS PDK
Esta guía te acompaña a través de un ejemplo de migración de un proyecto AWS PDK al Nx Plugin para AWS, además de proporcionar orientación general sobre este tema.
Migrar al Nx Plugin para AWS proporciona los siguientes beneficios sobre PDK:
- Compilaciones más rápidas
- Más fácil de usar (UI y CLI)
- Amigable con vibe-coding (¡prueba nuestro servidor MCP!)
- Tecnologías más modernas
- Desarrollo local de API y sitios web
- Más control (modifica archivos generados para adaptarlos a tu caso de uso)
- ¡Y más!
Ejemplo de Migración: Aplicación de Lista de Compras
Sección titulada «Ejemplo de Migración: Aplicación de Lista de Compras»En esta guía, usaremos la Aplicación de Lista de Compras del Tutorial de PDK como nuestro proyecto objetivo a migrar. Sigue los pasos en ese tutorial para crear el proyecto objetivo si deseas seguir la guía tú mismo.
La aplicación de lista de compras consiste en los siguientes tipos de proyecto PDK:
MonorepoTsProjectTypeSafeApiProjectCloudscapeReactTsWebsiteProjectInfrastructureTsProject
Crear Espacio de Trabajo
Sección titulada «Crear Espacio de Trabajo»Para comenzar, crearemos un nuevo espacio de trabajo para nuestro nuevo proyecto. Aunque es más extremo que una migración in situ, este enfoque nos da el resultado final más limpio. Crear un espacio de trabajo Nx es equivalente a usar el 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=cdkAbre el directorio shopping-list que este comando crea en tu IDE favorito.
Migrar la API
Sección titulada «Migrar la API»El TypeSafeApiProject utilizado en la aplicación de lista de compras hizo uso de:
- Smithy como lenguaje de modelado
- TypeScript para implementar operaciones
- Generación de hooks de TypeScript para integrar con un sitio web de react
Por lo tanto, podemos usar el generador ts#smithy-api para proporcionar funcionalidad equivalente.
Generar una API de Smithy con TypeScript
Sección titulada «Generar una API de Smithy con TypeScript»Ejecuta el generador ts#api con framework establecido en smithy para configurar tu proyecto de api en 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-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
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- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#api - Complete los parámetros requeridos
- name: api
- framework: smithy
- namespace: com.aws
- auth: iam
- Haga clic en
Generate
Notarás que esto genera un proyecto model, así como un proyecto backend. El proyecto model contiene tu modelo Smithy, y backend contiene tu implementación del servidor.
El backend utiliza el Smithy Server Generator for TypeScript. Exploraremos esto más a fondo a continuación.
Migrar el Modelo Smithy
Sección titulada «Migrar el Modelo Smithy»Ahora que tenemos la estructura básica para nuestro proyecto de API Smithy, podemos migrar el modelo:
-
Elimina los archivos Smithy de ejemplo generados en
packages/api/model/src -
Copia tu modelo del directorio
packages/api/model/src/main/smithydel proyecto PDK al directoriopackages/api/model/srcde tu nuevo proyecto. -
Actualiza el nombre del servicio y el espacio de nombres en
smithy-build.jsonpara que coincidan con la aplicación PDK:smithy-build.json "plugins": {"openapi": {"service": "com.aws#MyApi",... -
Actualiza el servicio en
main.smithypara agregar el errorValidationException, que es requerido al usar el Smithy TypeScript Server SDK.main.smithy use smithy.framework#ValidationException/// My Shopping List API@restJson1service MyApi {version: "1.0"operations: [GetShoppingListsPutShoppingListDeleteShoppingList]errors: [BadRequestErrorNotAuthorizedErrorInternalFailureErrorValidationException]} -
Agrega un archivo
extensions.smithyapackages/api/model/srcdonde definiremos un trait que proporciona información de paginación al cliente generado:extensions.smithy $version: "2"namespace com.awsuse smithy.openapi#specificationExtension@trait@specificationExtension(as: "x-cursor")structure cursor {inputToken: Stringenabled: Boolean} -
Agrega el nuevo trait
@cursora la operaciónGetShoppingListsenget-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}Cualquier operación
@paginatedtambién debe usar@cursorsi estás utilizando el generador de cliente proporcionado por el Nx Plugin for AWS (a través del generadorapi-connection). -
Finalmente, elimina el trait
@handlerde todas las operaciones ya que esto no es compatible con el Nx Plugin for AWS. Usandots#smithy-api, no necesitamos las construcciones CDK de funciones lambda generadas automáticamente ni los objetivos de empaquetado generados por este trait, ya que usamos un solo paquete para todas las funciones lambda.
En este punto, ejecutemos una compilación para verificar los cambios de nuestro modelo y asegurarnos de tener algo de código de servidor generado con el que trabajar. Habrá algunas fallas en el proyecto backend (@shopping-list/api) pero las abordaremos a continuación.
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildMigrar los Manejadores Lambda
Sección titulada «Migrar los Manejadores Lambda»Puedes considerar el proyecto api/backend como algo equivalente al proyecto api/handlers/typescript de Type Safe API.
Una de las principales diferencias entre Type Safe API y el generador ts#smithy-api es que los manejadores se implementan usando el Smithy Server Generator for TypeScript, en lugar de los envoltorios de manejadores generados propios de Type Safe API (que se encuentran en el proyecto api/generated/typescript/runtime).
Los manejadores lambda de la aplicación de lista de compras dependen del paquete @aws-sdk/client-dynamodb, así que instalémoslo en el proyecto @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/apiLuego, copiemos el archivo handlers/src/dynamo-client.ts del proyecto PDK a backend/src/operations para que esté disponible para nuestros manejadores.
El generador ts#smithy-api crea un ejemplo de operación Echo. Dado que eliminamos esto de nuestro modelo, elimina el manejador correspondiente en backend/src/operations/echo.ts. Registraremos nuestras operaciones migradas en service.ts más adelante.
Para migrar los manejadores, puedes seguir estos pasos generales:
-
Copia el manejador del directorio
packages/api/handlers/typescript/srcde tu proyecto PDK al directoriopackages/api/backend/src/operationsde tu nuevo proyecto. -
Elimina las importaciones de
my-api-typescript-runtimey en su lugar importa el tipo de operación del TypeScript Server SDK generado, así como elServiceContextpor ejemplo:import {deleteShoppingListHandler,DeleteShoppingListChainedHandlerFunction,INTERCEPTORS,Response,LoggingInterceptor,} from 'myapi-typescript-runtime';import { DeleteShoppingList as DeleteShoppingListOperation } from '../generated/ssdk/index.js';import { ServiceContext } from '../context.js'; -
Elimina la exportación del envoltorio del manejador
export const handler = deleteShoppingListHandler(...INTERCEPTORS,deleteShoppingList,); -
Actualiza la firma de tu manejador de operación para usar el SSDK:
export const deleteShoppingList: DeleteShoppingListChainedHandlerFunction = async (request) => {export const DeleteShoppingList: DeleteShoppingListOperation<ServiceContext> = async (input, ctx) => { -
Reemplaza el uso del
LoggingInterceptorconctx.logger. (También aplica a los interceptores de métricas y rastreo):LoggingInterceptor.getLogger(request).info('...');ctx.logger.info('...'); -
Actualiza las referencias a los parámetros de entrada. Dado que el SSDK proporciona tipos que coinciden exactamente con tu modelo Smithy (en lugar de agrupar los parámetros de ruta/consulta/encabezado por separado del parámetro del cuerpo), actualiza cualquier referencia de entrada en consecuencia:
const shoppingListId = request.input.requestParameters.shoppingListId;const shoppingListId = input.shoppingListId; -
Elimina el uso de
Response. En su lugar, simplemente devolvemos objetos simples en el SSDK.return Response.success({ shoppingListId });return { shoppingListId };También ya no lanzamos ni devolvemos
Response, en su lugar lanzamos los errores generados del 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' }); -
Actualiza cualquier importación para usar la sintaxis ESM, es decir, agregando la extensión
.jsa las importaciones relativas. -
Agrega la operación a
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,};
Migración de Manejadores de Lista de Compras
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, };};Generamos el proyecto de API Smithy con el nombre api inicialmente ya que queríamos que se agregara a packages/api para mantener la coherencia con el proyecto PDK. Dado que nuestra API Smithy ahora define service MyApi en lugar de service Api, necesitamos actualizar cualquier instancia de getApiServiceHandler con getMyApiServiceHandler.
Realiza este cambio en 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);Y en 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);Además, actualiza packages/api/backend/project.json y actualiza metadata.apiName a my-api:
"metadata": { "generator": "ts#smithy-api", "apiName": "api", "apiName": "my-api", "auth": "iam", "modelProject": "@shopping-list/api-model", "ports": [3001] },Verificar con una Compilación
Sección titulada «Verificar con una Compilación»Ahora podemos compilar el proyecto para verificar que la migración ha funcionado hasta ahora:
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildMigrar el Sitio Web
Sección titulada «Migrar el Sitio Web»El CloudscapeReactTsWebsiteProject utilizado en la aplicación de lista de compras configuró un sitio web React con CloudScape y autenticación Cognito integrada.
Este tipo de proyecto aprovechaba create-react-app, que ahora está obsoleto. Para migrar el sitio web en esta guía, utilizaremos el generador ts#website, que utiliza tecnologías más modernas y compatibles, específicamente Vite.
Como parte de la migración, también pasaremos del React Router configurado de PDK a TanStack Router, que agrega seguridad de tipos adicional al enrutamiento del sitio web.
Generar un Sitio Web React
Sección titulada «Generar un Sitio Web React»Ejecuta el generador ts#website con framework establecido en react para configurar tu proyecto de sitio web en packages/website. Dado que la aplicación de lista de compras está construida con componentes CloudScape, también establecemos ux en cloudscape (el valor predeterminado es 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-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
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- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#website - Complete los parámetros requeridos
- name: website
- framework: react
- ux: cloudscape
- Haga clic en
Generate
Agregar Autenticación Cognito
Sección titulada «Agregar Autenticación Cognito»El generador de sitio web React anterior no incluye autenticación cognito por defecto como CloudscapeReactTsWebsiteProject, en su lugar se agrega explícitamente a través del generador 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-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
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- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#website#auth - Complete los parámetros requeridos
- project: website
- cognitoDomain: shopping-list
- Haga clic en
Generate
Esto agrega componentes React que gestionan las redirecciones apropiadas para garantizar que los usuarios inicien sesión utilizando la interfaz de usuario alojada de Cognito. Esto también agrega una construcción CDK para implementar los recursos de Cognito en packages/common/constructs, llamada UserIdentity.
Conectar el Sitio Web a la API
Sección titulada «Conectar el Sitio Web a la API»En PDK podías pasar los proyectos Projen proporcionados entre sí para activar la generación de código de integración. Esto se usó en la aplicación de lista de compras para configurar el sitio web para poder integrarse con la API.
Con el Nx Plugin for AWS, la integración de API es compatible a través del generador connection. A continuación, usamos este generador para que nuestro sitio web pueda invocar nuestra 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-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
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- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - connection - Complete los parámetros requeridos
- sourceProject: website
- targetProject: api
- Haga clic en
Generate
Esto genera los proveedores de cliente necesarios y los objetivos de compilación para que tu sitio web llame a tu API a través de un cliente TypeScript generado.
Agregar Dependencia de AWS Northstar
Sección titulada «Agregar Dependencia de AWS Northstar»El CloudscapeReactTsWebsiteProject incluía automáticamente una dependencia de @aws-northstar/ui que se usa en nuestra aplicación de lista de compras, así que la agregamos al proyecto @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 incluye un componente de editor de código que depende de ace-builds, usando una importación específica de webpack que Vite no puede resolver. Dado que nuestra aplicación de lista de compras no usa este componente, lo excluimos del paquete agregándolo a la configuración external dentro de las opciones build existentes en packages/website/vite.config.mts:
build: { outDir: '../../dist/packages/website/bundle', emptyOutDir: true, reportCompressedSize: true, commonjsOptions: { transformMixedEsModules: true, }, rollupOptions: { external: ['ace-builds/webpack-resolver'], }, },Mover los Componentes y Páginas
Sección titulada «Mover los Componentes y Páginas»La aplicación de lista de compras tiene un componente llamado CreateItem, y dos páginas, ShoppingList y ShoppingLists. Migraremos estos al nuevo sitio web, haciendo algunos ajustes ya que estamos usando TanStack Router y el generador de código de cliente TypeScript del Nx Plugin for AWS.
-
Copia
packages/website/src/components/CreateItem/index.tsxdel proyecto PDK a la misma ubicación exacta en el nuevo proyecto. -
Copia
packages/website/src/pages/ShoppingLists/index.tsxapackages/website/src/routes/index.tsx, ya queShoppingListses nuestra página de inicio y usamos enrutamiento basado en archivos con TanStack router. -
Copia
packages/website/src/pages/ShoppingList/index.tsxapackages/website/src/routes/$shoppingListId.tsx, ya queShoppingListera la página que queremos mostrar en la ruta/:shoppingListId.
Ten en cuenta que ahora tendrás algunos errores de compilación visibles en tu IDE, necesitaremos hacer algunos cambios más para adaptarnos al nuevo marco, que se describen a continuación.
Migrar de React Router a TanStack Router
Sección titulada «Migrar de React Router a TanStack Router»Dado que estamos usando enrutamiento basado en archivos, podemos usar el servidor de desarrollo local del sitio web para gestionar la generación automática de la configuración de rutas.
Iniciemos el servidor del sitio web local:
pnpm nx dev websiteyarn nx dev websitenpx nx dev websitebunx nx dev websiteVerás algunos errores, pero el servidor del sitio web local debería iniciarse en el puerto 4200, así como el servidor de API Smithy local en el puerto 3001.
Sigue los pasos a continuación en routes/index.tsx y routes/$shoppingListId.tsx para migrar a TanStack Router:
-
Agrega
createFileRoutepara registrar cada ruta: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,});Después de guardar el archivo, notarás que los errores de tipo con la llamada a
createFileRoutehan desaparecido. -
Reemplaza el hook
useNavigate.Actualiza la importación:
import { useNavigate } from 'react-router-dom';import { useNavigate } from '@tanstack/react-router';Actualiza las llamadas al método
navigate(devuelto poruseNavigate) para pasar las rutas con seguridad de tipos:navigate(`/${cell.shoppingListId}`);navigate({to: '/$shoppingListId',params: { shoppingListId: cell.shoppingListId },}); -
Reemplaza el hook
useParams.Elimina la importación:
import { useParams } from 'react-router-dom';Actualiza las llamadas a
useParamscon el hook proporcionado por laRoutecreada anteriormente. ¡Ahora son seguras de tipos!const { shoppingListId } = useParams();const { shoppingListId } = Route.useParams();
Corregir las Importaciones de Componentes
Sección titulada «Corregir las Importaciones de Componentes»Dado que nuestros archivos de ruta no están tan profundamente anidados en el árbol de archivos como lo estaban en nuestro proyecto PDK, necesitamos corregir la importación de CreateItem tanto en routes/index.tsx como en routes/$shoppingListId.tsx:
import CreateItem from "../../components/CreateItem";import CreateItem from "../components/CreateItem";El AppLayoutContext también se proporciona en una ubicación ligeramente diferente en nuestro nuevo proyecto:
import { AppLayoutContext } from "../../layouts/App";import { AppLayoutContext } from "../components/AppLayout";Migrar para usar el nuevo Cliente TypeScript Generado
Sección titulada «Migrar para usar el nuevo Cliente TypeScript Generado»¡Nos estamos acercando ahora! A continuación, necesitamos migrar para usar el cliente TypeScript proporcionado por el Nx Plugin for AWS, que tiene algunas mejoras en comparación con Type Safe API. Para lograr esto, sigue los pasos a continuación
-
Importa el nuevo cliente y tipos generados en lugar de los antiguos, por ejemplo:
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";Ten en cuenta que
routes/$shoppingListId.tsximporta el tipoShoppingListcomo_ShoppingList- en ese archivo debemos hacer lo mismo, pero nuevamente importando desdetypes.gen.Ten en cuenta también que importamos los hooks relevantes directamente desde
@tanstack/react-query, ya que el cliente generado proporciona métodos para generar opciones para los hooks de TanStack query, en lugar de envoltorios de hooks. -
Instancia los nuevos hooks de TanStack Query, por ejemplo:
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(),); -
Elimina el envoltorio
<operation>RequestContentpara las llamadas a operaciones que aceptan parámetros en el cuerpo de la solicitud:await putShoppingList.mutateAsync({putShoppingListRequestContent: {name: item,},});
Migrar de TanStack Query v4 a v5
Sección titulada «Migrar de TanStack Query v4 a v5»Quedan algunos errores por corregir debido a las diferencias entre TanStack Query v4 (usado por PDK) y v5 que el generador connection agregó:
-
Reemplaza
isLoadingconisPendingpara mutaciones, por ejemplo:putShoppingList.isLoadingputShoppingList.isPending -
La aplicación de lista de compras hizo uso del
InfiniteQueryTablede@aws-northstar/uique espera un tipo de TanStack Query v4. Esto en realidad funciona con consultas infinitas de v5, así que simplemente podemos suprimir el error de tipo:<InfiniteQueryTablequery={getShoppingLists}query={getShoppingLists as any}
Visitar el Sitio Web Local
Sección titulada «Visitar el Sitio Web Local»Ahora puedes visitar el sitio web local en http://localhost:4200/
¡El sitio web debería cargarse ahora que todo ha sido migrado! Dado que la única infraestructura de la que depende la aplicación de lista de compras además de API, Website e Identity es la tabla DynamoDB - si tienes una tabla DynamoDB llamada shopping_list en la región, y credenciales locales de AWS que pueden acceder a ella, ¡el sitio web será completamente funcional!
Si no, está bien, migraremos la infraestructura a continuación.
Migración de Página de Lista de Compras
Página de Listas de Compras
/* 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,});Página de Lista de Compras
/* 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,});Migrar la Infraestructura
Sección titulada «Migrar la Infraestructura»El último proyecto que necesitamos migrar para nuestra aplicación de lista de compras es el InfrastructureTsProject. Este es un proyecto TypeScript CDK, para el cual el equivalente en Nx Plugin for AWS es el generador ts#infra.
Además de los proyectos Projen, PDK también proporcionaba construcciones CDK de las cuales estos proyectos dependen. Migraremos la aplicación de lista de compras de estas construcciones CDK también, en favor de las generadas por Nx Plugin for AWS.
Generar un Proyecto de Infraestructura TypeScript CDK
Sección titulada «Generar un Proyecto de Infraestructura TypeScript CDK»Ejecuta el generador ts#infra para configurar tu proyecto de infraestructura en 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-interactiveTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
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- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - ts#infra - Complete los parámetros requeridos
- name: infra
- Haga clic en
Generate
Migrar la Infraestructura CDK
Sección titulada «Migrar la Infraestructura CDK»La aplicación de lista de compras PDK instanciaba las siguientes construcciones dentro del stack de la aplicación CDK:
DatabaseConstructpara la tabla DynamoDB que almacena las listas de comprasUserIdentitypara recursos Cognito, importado directamente desde PDKMyApipara desplegar la API Smithy, que usaba la construcción TypeScript CDK generada con integraciones con seguridad de tipos, dependiendo de la construcción CDKTypeSafeRestApide PDK bajo el capó.Websitepara desplegar el sitio web, envolviendo la construcción CDKStaticWebsitede PDK.
A continuación, migraremos cada una de estas al nuevo proyecto.
Copiar el Application Stack
Sección titulada «Copiar el Application Stack»Copia packages/infra/src/stacks/application-stack.ts de la aplicación de lista de compras PDK a la misma ubicación exacta en tu nuevo proyecto. Verás algunos errores de TypeScript que abordaremos a continuación.
Copiar la Construcción Database
Sección titulada «Copiar la Construcción Database»La aplicación de lista de compras PDK tenía una construcción Database en packages/src/constructs/database.ts. Copia esto a la misma ubicación exacta en tu nuevo proyecto.
Dado que Nx Plugin for AWS usa Checkov para pruebas de seguridad, que es un poco más estricto que PDK Nag, también necesitamos agregar algunas supresiones:
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',);En application-stack.ts, actualiza la importación para DatabaseConstruct para usar sintaxis ESM:
import { DatabaseConstruct } from '../constructs/database';import { DatabaseConstruct } from '../constructs/database.js';Migrar la Construcción UserIdentity
Sección titulada «Migrar la Construcción UserIdentity»La construcción UserIdentity generalmente puede intercambiarse sin cambios ajustando las importaciones.
import { UserIdentity } from "@aws/pdk/identity";import { UserIdentity } from '@shopping-list/common-constructs';...const userIdentity = new UserIdentity(this, `${id}UserIdentity`);Ten en cuenta que las construcciones subyacentes utilizadas por la nueva construcción UserIdentity se proporcionan directamente desde aws-cdk-lib, donde PDK usaba @aws-cdk/aws-cognito-identitypool-alpha.
Migrar la Construcción API
Sección titulada «Migrar la Construcción API»La aplicación de lista de compras PDK tenía una construcción en constructs/apis/myapi.ts que instanciaba una construcción CDK que Type Safe API generaba a partir de tu modelo Smithy.
Además de esta construcción, dado que el proyecto PDK usaba el trait @handler, también se generaban construcciones CDK de funciones lambda generadas.
Al igual que Type Safe API, Nx Plugin for AWS proporciona seguridad de tipos para integraciones basadas en tu modelo Smithy, sin embargo, se logra de una manera mucho más simple y flexible. En lugar de generar una construcción CDK completa en tiempo de compilación, solo se generan “metadatos” mínimos, que packages/common/constructs/src/app/apis/api.ts usa de manera genérica. Puedes aprender más sobre cómo usar la construcción en la guía del generador ts#smithy-api.
Sigue los siguientes pasos:
-
Instancia la construcción
Apienapplication-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(),});Observa aquí que usamos
Api.defaultIntegrations(this).build()- el comportamiento predeterminado es crear una función lambda para cada operación en nuestra API, que es el mismo comportamiento que teníamos enmyapi.ts. -
Otorga permisos para que las funciones lambda accedan a la tabla DynamoDB.
En la aplicación de lista de compras PDK, el
DatabaseConsructse pasaba aMyApi, y este gestionaba la adición de los permisos relevantes a cada construcción de función generada. Haremos esto directamente en el archivoapplication-stack.tsaccediendo a la propiedadintegrationscon seguridad de tipos de la construcciónApi: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)); -
Otorga permisos para que los usuarios autenticados invoquen la API.
Dentro del
myapi.tsde la aplicación PDK, también se otorgaban permisos IAM a los usuarios autenticados para invocar la API. Haremos el equivalente enapplication-stack.ts:stacks/application-stack.ts api.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
Migrar la Construcción Website
Sección titulada «Migrar la Construcción Website»Finalmente, agregamos la construcción Website de packages/common/constructs/src/app/static-websites/website.ts a application-stack.ts, ya que este es el equivalente de packages/infra/src/constructs/websites/website.ts de la aplicación de lista de compras PDK.
import { Website } from "../constructs/websites/website";import { Website } from '@shopping-list/common-constructs';...new Website(this, "Website", { userIdentity, myapi,});new Website(this, 'Website');Observa que no pasamos la identidad o la API al sitio web - la configuración en tiempo de ejecución se gestiona dentro de cada construcción proporcionada por Nx Plugin for AWS, donde UserIdentity y Api registran los valores necesarios, y Website gestiona su despliegue en /runtime-config.json en tu sitio web estático.
Construyamos el proyecto ahora que hemos migrado todas las partes relevantes de la base de código a nuestro nuevo proyecto.
pnpm nx run-many --target buildyarn nx run-many --target buildnpx nx run-many --target buildbunx nx run-many --target buildDesplegar
Sección titulada «Desplegar»Ahora que tenemos nuestra base de código completamente migrada, podemos proceder a desplegarla. Hay dos caminos que podemos tomar en este punto.
Todos los Recursos Nuevos (Simple)
Sección titulada «Todos los Recursos Nuevos (Simple)»El enfoque más simple es tratar esto como una aplicación completamente nueva, lo que significa que “empezaremos de nuevo” con una tabla DynamoDB y un Cognito User Pool nuevos, perdiendo todos los usuarios y sus listas de compras. Para este enfoque, simplemente:
-
Elimina la tabla DynamoDB llamada
shopping_list -
Despliega la nueva aplicación:
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/*
🎉 ¡Y hemos terminado! 🎉
Migrar Recursos con Estado Existentes sin Interrupción (Más Complejo)
Sección titulada «Migrar Recursos con Estado Existentes sin Interrupción (Más Complejo)»En realidad, es más probable que desees migrar los recursos de AWS existentes para que sean administrados por la nueva base de código, evitando cualquier tiempo de inactividad para tus clientes.
Para nuestra aplicación de lista de compras, los recursos con estado que nos importan son la tabla DynamoDB que contiene las listas de compras de nuestros usuarios, y el User Pool que contiene los detalles de todos nuestros usuarios registrados. Nuestro plan de alto nivel será retener estos dos recursos clave y moverlos para que sean administrados por nuestro nuevo stack, luego actualizar el DNS para que apunte a nuestro nuevo sitio web (y API si está expuesta a los clientes).
-
Actualiza tu nueva aplicación para hacer referencia a los recursos existentes que deseas retener.
Para la aplicación de lista de compras, hacemos esto para la tabla DynamoDB
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Y para el 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>',); -
Construye y despliega la nueva aplicación:
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/*Ahora tenemos nuestra nueva aplicación levantada haciendo referencia a los recursos existentes, aún sin recibir tráfico.
-
Realiza pruebas de integración completas para asegurar que la nueva aplicación funcione como se espera. Para la aplicación de lista de compras, carga el sitio web y verifica que puedas iniciar sesión y crear, ver, editar y eliminar listas de compras.
-
Revierte los cambios que hacen referencia a los recursos existentes en tu nueva aplicación, pero no los despliegues todavía.
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Y para el 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>',);Y luego ejecuta una construcción
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 -
Usa
cdk importen la carpetapackages/infrade tu nueva aplicación para ver qué recursos se nos solicitará importar.New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --forceAvanza por los prompts presionando enter. La importación fallará porque los recursos están administrados por otro stack - esto es esperado, solo hicimos este paso para confirmar qué recursos necesitaremos retener. Verás una salida como esta:
Ventana 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) yEsto nos dice que en realidad hay 3 recursos que necesitaremos importar a nuestro nuevo stack.
-
Actualiza tu antiguo proyecto PDK para establecer
RemovalPolicyenRETAINpara los recursos descubiertos en el paso anterior. Al momento de escribir esto, este es el valor predeterminado tanto para el User Pool como para la tabla DynamoDB, pero necesitamos actualizarlo para el SMS Role que descubrimos arriba: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); -
Despliega tu proyecto PDK para que se apliquen las políticas de eliminación
PDK Application cd packages/infranpx projen deploy -
Echa un vistazo a la consola de CloudFormation y registra los valores que se te solicitaron en el paso
cdk importanterior- El User Pool ID, ej.
us-west-2_XXXXX - El SMS Role Name, ej.
infra-sandbox-UserIdentityUserPoolsmsRoleXXXXXX
- El User Pool ID, ej.
-
Actualiza tu proyecto PDK para hacer referencia a los recursos existentes en lugar de crearlos
constructs/database.ts this.shoppingListTable = new Table(this, 'ShoppingList', {...this.shoppingListTable = Table.fromTableName(this,'ShoppingList','shopping_list',);Y para el 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,}); -
Despliega tu proyecto PDK nuevamente, esto significará que los recursos ya no están administrados por el stack de CloudFormation de nuestro proyecto PDK.
PDK Application cd packages/infranpx projen deploy -
Ahora que los recursos no están administrados, podemos ejecutar
cdk importen nuestra nueva aplicación para realizar realmente la importación:New Application cd packages/infrapnpm exec cdk import shopping-list-infra-sandbox/Application --forceIngresa los valores cuando se te solicite, la importación debería completarse exitosamente.
-
Despliega la nueva aplicación nuevamente para asegurarte de que se realicen los cambios en estos recursos existentes (ahora administrados por tu nuevo stack):
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/* -
Realiza una prueba completa de tu nueva aplicación nuevamente
-
Actualiza los registros DNS para que apunten a tu nuevo sitio web (y API si es necesario).
Recomendamos un enfoque gradual usando Weighted Routing de Route53, mediante el cual una fracción de las solicitudes se dirigen a la nueva aplicación para comenzar. A medida que monitoreas tus métricas, puedes aumentar el peso para la nueva aplicación hasta que no se envíe tráfico a tu antigua aplicación PDK.
Si no tienes ningún DNS y usaste los dominios generados automáticamente para el sitio web y la API, siempre puedes considerar hacer proxy de las solicitudes (por ejemplo, a través de un CloudFront HTTP origin o API Gateway HTTP integration(s)).
-
Monitorea las métricas de la aplicación PDK para asegurar que no haya tráfico, y finalmente destruye el antiguo stack de CloudFormation:
Ventana de terminal cd packages/infranpx projen destroy
Eso fue un poco más complicado, ¡pero migramos exitosamente a nuestros usuarios sin problemas a la nueva aplicación! 🎉🎉🎉
Ahora tenemos los nuevos beneficios del Nx Plugin for AWS sobre PDK:
- Construcciones más rápidas
- Soporte para desarrollo local de API
- Una base de código amigable para vibe-coding (¡prueba nuestro servidor MCP!)
- Código cliente/servidor con tipado seguro más intuitivo
- ¡Y más!
Preguntas Frecuentes
Sección titulada «Preguntas Frecuentes»Esta sección proporciona orientación para características de PDK que no están cubiertas por el ejemplo de migración anterior.
Como regla general al pasar de PDK, recomendamos comenzar cualquier proyecto con un Espacio de Trabajo Nx, dadas sus similitudes con el Monorepo de PDK. También recomendamos usar nuestros generadores como las primitivas sobre las cuales construir cualquier nuevo tipo.
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
Sección titulada «CDK Graph»CDK Graph construye gráficos de tus recursos CDK conectados, y proporcionó dos plugins:
Diagram Plugin
Sección titulada «Diagram Plugin»El CDK Graph Diagram Plugin genera diagramas de arquitectura de AWS a partir de tu infraestructura CDK.
Para un enfoque determinístico similar, una alternativa viable es CDK-Dia.
Con los avances en IA Generativa, muchos modelos fundacionales son capaces de crear diagramas de alta calidad a partir de tu infraestructura CDK. Recomendamos probar el AWS Diagram MCP Server. Consulta esta publicación del blog para un tutorial.
Threat Composer Plugin
Sección titulada «Threat Composer Plugin»El CDK Graph Threat Composer Plugin genera un Threat Composer inicial de modelo de amenazas a partir de tu código CDK.
Este plugin funcionaba simplemente filtrando un modelo de amenazas base que contenía amenazas de ejemplo, y filtrándolas según los recursos que tu stack utilizaba.
Si estás interesado en estas amenazas de ejemplo específicas, puedes copiar y filtrar el modelo de amenazas base, o usarlo como contexto para ayudar a un modelo fundacional a generar uno similar.
AWS Arch
Sección titulada «AWS Arch»AWS Arch proporcionaba mapeos entre recursos de CloudFormation y sus iconos de arquitectura asociados para CDK Graph anteriormente.
Consulta la página de iconos de arquitectura de AWS para recursos relacionados con iconos. Diagrams también proporciona una forma de construir diagramas como código.
Si estabas usando esto directamente, ¡considera hacer un fork del proyecto y tomar propiedad del mismo!
Pipeline
Sección titulada «Pipeline»PDK proporcionaba un PDKPipelineProject que configuraba un proyecto de infraestructura CDK y hacía uso de un constructo CDK que envolvía algunos recursos de CDK Pipelines.
Para migrar desde esto, puedes usar los constructos de CDK Pipelines directamente. Sin embargo, en la práctica es probable que sea más sencillo usar algo como GitHub actions o GitLab CI/CD, donde defines CDK Stages y ejecutas el comando deploy para el stage apropiado directamente.
PDK Nag
Sección titulada «PDK Nag»PDK Nag envuelve CDK Nag, y proporciona un conjunto de reglas específicas para construir prototipos.
Para migrar desde PDK Nag, usa CDK Nag directamente. Si necesitas el mismo conjunto de reglas, puedes crear un “pack” propio siguiendo la documentación aquí.
Type Safe API
Sección titulada «Type Safe API»Los componentes más comúnmente utilizados de Type Safe API están cubiertos en el ejemplo de migración anterior, sin embargo, hay otras características para las cuales los detalles de migración se encuentran a continuación.
APIs Modeladas con OpenAPI
Sección titulada «APIs Modeladas con OpenAPI»El Nx Plugin for AWS admite APIs modeladas en Smithy, pero no aquellas modeladas directamente en OpenAPI. El generador ts#smithy-api es un buen punto de partida que luego puedes modificar. Puedes definir tu especificación OpenAPI en la carpeta src del proyecto model en lugar de Smithy, y modificar el build.Dockerfile para usar tu herramienta de generación de código deseada para clientes/servidores si no están disponibles en NPM. Si tus herramientas deseadas están en NPM, simplemente puedes instalarlas como dependencias de desarrollo en tu espacio de trabajo Nx y llamarlas directamente como objetivos de compilación de Nx.
Backend
Sección titulada «Backend»Para backends type-safe modelados en OpenAPI, puedes considerar usar uno de los Generadores de Servidor de OpenAPI Generator. Estos no generarán directamente para AWS Lambda, pero puedes usar el AWS Lambda Web Adapter para cerrar la brecha para muchos de ellos.
Para clientes TypeScript, puedes usar el generador ts#website y el generador connection con un ejemplo de ts#api (con framework configurado como smithy) para ver cómo se generan e integran los clientes con un sitio web. Esto configura objetivos de compilación que generan clientes invocando nuestros generadores open-api#ts-client u open-api#ts-hooks. Puedes usar estos generadores tú mismo apuntándolos a tu Especificación OpenAPI.
Para otros lenguajes, también puedes ver si alguno de los generadores de OpenAPI Generator se ajusta a tus necesidades.
También puedes construir un generador personalizado usando el generador ts#nx-generator. Consulta la documentación de ese generador para obtener detalles sobre cómo generar código desde OpenAPI. Puedes usar las plantillas del Nx Plugin for AWS como punto de partida. También puedes incluso consultar las plantillas del código base de PDK para más inspiración, teniendo en cuenta que la estructura de datos sobre la que operan las plantillas es un poco diferente al Nx Plugin for AWS.
APIs Modeladas con TypeSpec
Sección titulada «APIs Modeladas con TypeSpec»Para TypeSpec, la sección anterior para OpenAPI también aplica. Puedes comenzar generando un ts#smithy-api, instalar el compilador TypeSpec y los paquetes OpenAPI en tu espacio de trabajo Nx, y actualizar el objetivo compile del proyecto model para ejecutar tsp compile en su lugar, asegurándote de que genere una especificación OpenAPI en el directorio dist.
Backend
Sección titulada «Backend»El enfoque recomendado sería usar el generador de servidor HTTP TypeSpec para JavaScript para generar tu código de servidor, ya que esto funciona directamente en tu modelo TypeSpec.
Puedes usar el AWS Lambda Web Adapter para ejecutar el servidor generado en AWS Lambda.
También puedes usar cualquiera de las opciones de OpenAPI anteriores.
TypeSpec tiene sus propios generadores de código para clientes en los tres lenguajes admitidos por Type Safe API:
La sección de OpenAPI anterior también aplica ya que TypeSpec puede compilar a OpenAPI.
APIs Modeladas con Smithy
Sección titulada «APIs Modeladas con Smithy»El ejemplo de migración anterior describe la migración para usar el generador ts#smithy-api. Esta sección cubre las opciones para backends y clientes de Python y Java.
Backend
Sección titulada «Backend»El generador de código Smithy para Java. Este tiene un generador de servidor Java así como un adaptador para ejecutar el servidor Java generado en AWS Lambda.
Smithy no tiene un generador de servidor para Python, por lo que necesitarás ir a través de OpenAPI. Consulta la sección anterior sobre APIs Modeladas con OpenAPI para opciones potenciales.
El generador de código Smithy para Java. Este tiene un generador de cliente Java.
Para clientes Python, puedes revisar Smithy Python.
Para TypeScript, revisa Smithy TypeScript, o usa el mismo enfoque que hemos tomado en ts#smithy-api yendo a través de OpenAPI (optamos por esto ya que nos da consistencia entre APIs tRPC, FastAPI y Smithy a través de hooks de TanStack Query).
Smithy Shape Library
Sección titulada «Smithy Shape Library»Type Safe API proporcionaba un tipo de proyecto Projen llamado SmithyShapeLibraryProject que configuraba un proyecto que contenía modelos Smithy que podían ser reutilizados por múltiples APIs basadas en Smithy.
El equivalente es el generador smithy#project con type configurado como 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=shapesTambién puede realizar una ejecución en seco para ver qué archivos se cambiarían
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- Instale el Nx Console VSCode Plugin si aún no lo ha hecho
- Abra la consola Nx en VSCode
- Haga clic en
Generate (UI)en la sección "Common Nx Commands" - Busque
@aws/nx-plugin - smithy#project - Complete los parámetros requeridos
- name: my-shapes
- type: shapes
- Haga clic en
Generate
Mueve las formas de tu SmithyShapeLibraryProject a la carpeta src del proyecto generado, luego consulta la guía del proyecto Smithy para saber cómo conectar la biblioteca como una dependencia del modelo de tu API.
Interceptors
Sección titulada «Interceptors»Type Safe API proporcionaba los siguientes interceptores predeterminados:
- Interceptores de registro, rastreo y métricas usando Powertools for AWS Lambda
- Interceptor try-catch para manejar excepciones no capturadas
- Interceptor CORS para devolver encabezados CORS
El generador ts#smithy-api instrumenta registro, rastreo y métricas con Powertools for AWS Lambda usando Middy. El comportamiento del interceptor try-catch está integrado en el Smithy TypeScript SSDK, y los encabezados CORS se agregan en handler.ts.
Para interceptores de registro, rastreo y métricas en cualquier lenguaje, usa Powertools for AWS Lambda directamente.
Para migrar interceptores personalizados, recomendamos usar las siguientes bibliotecas:
- TypeScript - Middy
- Python - Powertools for AWS Lambda Middleware Factory
- Java - Instrumenta métodos antes/después de tu lógica de negocio usando aws-lambda-java-libs para un enfoque simple, o considera AspectJ para construir tu middleware como anotaciones.
Generación de Documentación
Sección titulada «Generación de Documentación»Type Safe API proporcionaba generación de documentación usando Redocly CLI. Esto es muy fácil de agregar a un proyecto existente una vez que lo hayas migrado como se indicó anteriormente.
-
Instala el 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 -
Agrega un objetivo de generación de documentación a tu proyecto
modelusandoredocly build-docs, por ejemplo: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"]}}
También puedes considerar los generadores de documentación de OpenAPI Generator.
Mock Integrations
Sección titulada «Mock Integrations»Type Safe API generaba mocks para ti dentro de su paquete de infraestructura generado.
Puedes migrar a JSON Schema Faker que puede crear los datos mock basados en JSON Schemas. Esto puede funcionar directamente en una especificación OpenAPI, y tiene una CLI que podrías ejecutar como parte de la compilación de tu proyecto model.
Puedes actualizar tu infraestructura CDK para leer el archivo JSON generado por JSON Schema Faker, y devolver la MockIntegration de API Gateway apropiada para una integración, basándote en el metadata.gen.ts generado (asumiendo que usaste el generador ts#smithy-api).
Backends de Lenguaje Mixto
Sección titulada «Backends de Lenguaje Mixto»Type Safe API admitía la implementación de APIs con una mezcla de diferentes lenguajes en el backend. Esto también se puede lograr proporcionando “overrides” a las integraciones al instanciar tu constructo API en 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(),});Necesitarás “stub” tu servicio/enrutador para que tu servicio compile si usas el ts#smithy-api y el TypeScript Server SDK, por ejemplo:
export const Service: ApiService<ServiceContext> = { ... Echo: () => { throw new Error(`Not Implemented`); },};Validación de Entrada
Sección titulada «Validación de Entrada»Type Safe API agregaba validación nativa de API Gateway para cuerpos de solicitud basados en tu especificación OpenAPI ya que usaba el constructo SpecRestApi internamente.
Con el generador ts#smithy-api, la validación es realizada por el Server SDK mismo. Esto es lo mismo para la mayoría de los generadores de servidor.
Si deseas implementar validación nativa de API Gateway, podrías hacerlo modificando packages/common/constructs/src/core/api/rest-api.ts para leer el JSON schema relevante para el cuerpo de solicitud de cada operación desde tu especificación OpenAPI.
APIs WebSocket
Sección titulada «APIs WebSocket»Desafortunadamente no hay una ruta de migración directa para la API websocket de Type Safe API usando API Gateway y Lambda con desarrollo de API basado en modelos. Sin embargo, esta sección de la guía tiene como objetivo al menos ofrecer algunas ideas.
Considera usar AsyncAPI para modelar tu API en lugar de OpenAPI o TypeSpec ya que está diseñado para manejar APIs asíncronas. La Plantilla NodeJS de AsyncAPI puede generar un backend de websocket Node que podrías alojar en ECS por ejemplo.
También puedes considerar AppSync Events para infraestructura, y usar Powertools. ¡Esta publicación de blog vale la pena leer!
Otra opción es usar APIs GraphQL con websockets en AppSync, para lo cual tenemos un issue de GitHub al que puedes dar +1! Consulta la guía del desarrollador de AppSync para detalles y enlaces a proyectos de ejemplo.
También puedes considerar crear tus propios generadores de código que interpreten las mismas extensiones de proveedor que Type Safe API. Consulta la sección APIs Modeladas con OpenAPI para detalles sobre la construcción de generadores personalizados basados en OpenAPI. Puedes encontrar las plantillas que Type Safe API usa para los manejadores Lambda de API Gateway Websocket API aquí, y el cliente aquí.
También puedes considerar migrar para usar el generador ts#trpc-api para usar tRPC. Al momento de escribir esto, aún no tenemos soporte para suscripciones/streaming pero si esto es algo que necesitas, agrega un +1 a nuestro issue de GitHub que rastrea esto.
Smithy es agnóstico al protocolo, pero aún no tiene soporte para el protocolo Websocket, consulta este issue de GitHub que rastrea el soporte.
Infraestructura en Python o Java
Sección titulada «Infraestructura en Python o Java»PDK soportaba infraestructura CDK escrita en Python y Java. No soportamos esto en el Nx Plugin for AWS al momento de escribir esto.
El camino recomendado sería migrar tu infraestructura CDK a TypeScript, o usar nuestros generadores y migrar el paquete de construcciones comunes a tu lenguaje deseado. Puedes usar IA Generativa para acelerar este tipo de migraciones, por ejemplo Kiro CLI. Puedes hacer que un agente de IA itere sobre la migración hasta que las plantillas de CloudFormation sintetizadas sean idénticas.
Lo mismo aplica para la infraestructura generada de Type Safe API en Python o Java - puedes traducir la construcción genérica rest-api.ts del paquete de construcciones comunes, e implementar tu propio generador de metadatos simple para tu lenguaje objetivo (consulta la sección APIs Modelled with OpenAPI).
Puedes usar el generador py#project para un proyecto base de Python al que agregar tu código CDK (y mover tu archivo cdk.json, agregando los targets relevantes). Puedes usar el plugin @nx/gradle de Nx para proyectos Java, o @jnxplus/nx-maven para Maven.
Uso de Projen
Sección titulada «Uso de Projen»PDK fue construido sobre Projen. Projen y Nx Generators tienen diferencias bastante fundamentales, lo que significa que aunque es técnicamente posible combinarlos, es probable que sea un anti-patrón. Projen gestiona los archivos del proyecto como código de tal manera que no pueden ser modificados directamente, mientras que los generadores de Nx proporcionan archivos de proyecto una vez y luego el código puede ser modificado libremente.
Si desea continuar usando Projen, puede implementar los tipos de proyecto Projen deseados usted mismo. Para seguir los patrones del Nx Plugin for AWS, puede ejecutar nuestros generadores o examinar su código fuente en GitHub para ver cómo se construyen los tipos de proyecto deseados, e implementar las partes relevantes usando las primitivas de Projen.